From be7ee96db339115e341fffc345e80667392795ae Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:08:57 -0400 Subject: [PATCH 01/29] chore(go): update go-sdk seed (#13521) Co-authored-by: patrickthornton --- .../omit-fern-headers/core/console_logger.go | 66 -------- .../imdb/omit-fern-headers/core/log_config.go | 94 ----------- .../imdb/omit-fern-headers/core/log_level.go | 50 ------ .../imdb/omit-fern-headers/core/logger.go | 98 ------------ .../core/logging_http_client.go | 150 ------------------ .../omit-fern-headers/core/request_option.go | 10 -- seed/go-sdk/imdb/omit-fern-headers/go.mod | 2 +- .../option/request_option.go | 17 -- 8 files changed, 1 insertion(+), 486 deletions(-) delete mode 100644 seed/go-sdk/imdb/omit-fern-headers/core/console_logger.go delete mode 100644 seed/go-sdk/imdb/omit-fern-headers/core/log_config.go delete mode 100644 seed/go-sdk/imdb/omit-fern-headers/core/log_level.go delete mode 100644 seed/go-sdk/imdb/omit-fern-headers/core/logger.go delete mode 100644 seed/go-sdk/imdb/omit-fern-headers/core/logging_http_client.go diff --git a/seed/go-sdk/imdb/omit-fern-headers/core/console_logger.go b/seed/go-sdk/imdb/omit-fern-headers/core/console_logger.go deleted file mode 100644 index 8488fb0471d4..000000000000 --- a/seed/go-sdk/imdb/omit-fern-headers/core/console_logger.go +++ /dev/null @@ -1,66 +0,0 @@ -package core - -import ( - "fmt" - "io" - "os" - "sync" -) - -// ConsoleLogger is the default ILogger implementation that writes to stderr. -// It uses a simple format of "LEVEL - message". -type ConsoleLogger struct { - mu sync.Mutex - output io.Writer -} - -// ConsoleLoggerOption configures a ConsoleLogger. -type ConsoleLoggerOption func(*ConsoleLogger) - -// WithConsoleLoggerOutput sets the output writer for the console logger. -func WithConsoleLoggerOutput(output io.Writer) ConsoleLoggerOption { - return func(l *ConsoleLogger) { - l.output = output - } -} - -// NewConsoleLogger creates a new ConsoleLogger instance. -// By default, it writes to stderr. -func NewConsoleLogger(opts ...ConsoleLoggerOption) *ConsoleLogger { - logger := &ConsoleLogger{ - output: os.Stderr, - } - for _, opt := range opts { - opt(logger) - } - return logger -} - -// Debug logs a debug message. -func (l *ConsoleLogger) Debug(msg string) { - l.log("DEBUG", msg) -} - -// Info logs an informational message. -func (l *ConsoleLogger) Info(msg string) { - l.log("INFO", msg) -} - -// Warn logs a warning message. -func (l *ConsoleLogger) Warn(msg string) { - l.log("WARN", msg) -} - -// Error logs an error message. -func (l *ConsoleLogger) Error(msg string) { - l.log("ERROR", msg) -} - -func (l *ConsoleLogger) log(level, msg string) { - l.mu.Lock() - defer l.mu.Unlock() - _, _ = fmt.Fprintf(l.output, "%s - %s\n", level, msg) -} - -// Ensure ConsoleLogger implements ILogger interface. -var _ ILogger = (*ConsoleLogger)(nil) diff --git a/seed/go-sdk/imdb/omit-fern-headers/core/log_config.go b/seed/go-sdk/imdb/omit-fern-headers/core/log_config.go deleted file mode 100644 index 5d64f55e25e3..000000000000 --- a/seed/go-sdk/imdb/omit-fern-headers/core/log_config.go +++ /dev/null @@ -1,94 +0,0 @@ -package core - -// LogConfig configures logging for the SDK. -// Use the builder to configure logging behavior. -// -// Example: -// -// config := NewLogConfigBuilder(). -// Level(LogLevelDebug). -// Silent(false). -// Build() -// -// Or with a custom logger: -// -// config := NewLogConfigBuilder(). -// Level(LogLevelDebug). -// Logger(myCustomLogger). -// Silent(false). -// Build() -// -// Defaults: -// - Level: LogLevelInfo -// - Logger: ConsoleLogger (writes to stderr) -// - Silent: true (no output unless explicitly enabled) -type LogConfig struct { - level LogLevel - logger ILogger - silent bool -} - -// Level returns the configured log level. -func (c *LogConfig) Level() LogLevel { - return c.level -} - -// Logger returns the configured logger. -func (c *LogConfig) Logger() ILogger { - return c.logger -} - -// Silent returns whether logging is disabled. -func (c *LogConfig) Silent() bool { - return c.silent -} - -// LogConfigBuilder is used to build a LogConfig. -type LogConfigBuilder struct { - level LogLevel - logger ILogger - silent bool -} - -// NewLogConfigBuilder creates a new builder for LogConfig. -func NewLogConfigBuilder() *LogConfigBuilder { - return &LogConfigBuilder{ - level: LogLevelInfo, - logger: NewConsoleLogger(), - silent: true, - } -} - -// Level sets the minimum log level. Only messages at this level or above will be logged. -// Defaults to LogLevelInfo. -func (b *LogConfigBuilder) Level(level LogLevel) *LogConfigBuilder { - b.level = level - return b -} - -// Logger sets a custom logger implementation. Defaults to ConsoleLogger. -func (b *LogConfigBuilder) Logger(logger ILogger) *LogConfigBuilder { - b.logger = logger - return b -} - -// Silent sets whether logging is silent (disabled). Defaults to true. -// Set to false to enable log output. -func (b *LogConfigBuilder) Silent(silent bool) *LogConfigBuilder { - b.silent = silent - return b -} - -// Build creates the LogConfig with the configured settings. -func (b *LogConfigBuilder) Build() *LogConfig { - return &LogConfig{ - level: b.level, - logger: b.logger, - silent: b.silent, - } -} - -// createLogger creates a Logger wrapper from the LogConfig. -func (c *LogConfig) createLogger() *Logger { - return NewLogger(c.logger, c.level, c.silent) -} diff --git a/seed/go-sdk/imdb/omit-fern-headers/core/log_level.go b/seed/go-sdk/imdb/omit-fern-headers/core/log_level.go deleted file mode 100644 index 35ac7f9a739a..000000000000 --- a/seed/go-sdk/imdb/omit-fern-headers/core/log_level.go +++ /dev/null @@ -1,50 +0,0 @@ -package core - -import "strings" - -// LogLevel represents the severity level for log messages. -// Silent by default — no log output unless explicitly configured. -type LogLevel int - -const ( - // LogLevelDebug is the lowest log level, used for detailed debugging information. - LogLevelDebug LogLevel = iota + 1 - // LogLevelInfo is used for general informational messages. - LogLevelInfo - // LogLevelWarn is used for warning messages that indicate potential issues. - LogLevelWarn - // LogLevelError is used for error messages that indicate serious problems. - LogLevelError -) - -// String returns the string representation of the log level. -func (l LogLevel) String() string { - switch l { - case LogLevelDebug: - return "DEBUG" - case LogLevelInfo: - return "INFO" - case LogLevelWarn: - return "WARN" - case LogLevelError: - return "ERROR" - default: - return "UNKNOWN" - } -} - -// ParseLogLevel parses a log level from a string (case-insensitive). -func ParseLogLevel(level string) LogLevel { - switch strings.ToLower(level) { - case "debug": - return LogLevelDebug - case "info": - return LogLevelInfo - case "warn": - return LogLevelWarn - case "error": - return LogLevelError - default: - return LogLevelInfo - } -} diff --git a/seed/go-sdk/imdb/omit-fern-headers/core/logger.go b/seed/go-sdk/imdb/omit-fern-headers/core/logger.go deleted file mode 100644 index 0fd6bd547492..000000000000 --- a/seed/go-sdk/imdb/omit-fern-headers/core/logger.go +++ /dev/null @@ -1,98 +0,0 @@ -package core - -// ILogger is the interface for logging SDK operations. -// Implement this interface to provide custom logging backend. -type ILogger interface { - // Debug logs a debug message. - Debug(msg string) - // Info logs an informational message. - Info(msg string) - // Warn logs a warning message. - Warn(msg string) - // Error logs an error message. - Error(msg string) -} - -// Logger is a wrapper around ILogger that provides level-based logging. -type Logger struct { - logger ILogger - level LogLevel - silent bool -} - -// NewLogger creates a new Logger wrapper. -func NewLogger(logger ILogger, level LogLevel, silent bool) *Logger { - return &Logger{ - logger: logger, - level: level, - silent: silent, - } -} - -// Debug logs a debug message if debug logging is enabled. -func (l *Logger) Debug(msg string) { - if l.silent { - return - } - if l.shouldLog(LogLevelDebug) { - l.logger.Debug(msg) - } -} - -// Info logs an info message if info logging is enabled. -func (l *Logger) Info(msg string) { - if l.silent { - return - } - if l.shouldLog(LogLevelInfo) { - l.logger.Info(msg) - } -} - -// Warn logs a warning message if warn logging is enabled. -func (l *Logger) Warn(msg string) { - if l.silent { - return - } - if l.shouldLog(LogLevelWarn) { - l.logger.Warn(msg) - } -} - -// Error logs an error message if error logging is enabled. -func (l *Logger) Error(msg string) { - if l.silent { - return - } - if l.shouldLog(LogLevelError) { - l.logger.Error(msg) - } -} - -// IsDebug returns true if debug logging is enabled. -func (l *Logger) IsDebug() bool { - return !l.silent && l.shouldLog(LogLevelDebug) -} - -// IsInfo returns true if info logging is enabled. -func (l *Logger) IsInfo() bool { - return !l.silent && l.shouldLog(LogLevelInfo) -} - -// IsWarn returns true if warn logging is enabled. -func (l *Logger) IsWarn() bool { - return !l.silent && l.shouldLog(LogLevelWarn) -} - -// IsError returns true if error logging is enabled. -func (l *Logger) IsError() bool { - return !l.silent && l.shouldLog(LogLevelError) -} - -// shouldLog checks if a message at the given level should be logged. -func (l *Logger) shouldLog(level LogLevel) bool { - return level >= l.level -} - -// Ensure Logger implements ILogger interface. -var _ ILogger = (*Logger)(nil) diff --git a/seed/go-sdk/imdb/omit-fern-headers/core/logging_http_client.go b/seed/go-sdk/imdb/omit-fern-headers/core/logging_http_client.go deleted file mode 100644 index 0ed2ebcecc93..000000000000 --- a/seed/go-sdk/imdb/omit-fern-headers/core/logging_http_client.go +++ /dev/null @@ -1,150 +0,0 @@ -package core - -import ( - "fmt" - "net/http" - "sort" - "strings" -) - -// Sensitive headers that should be redacted in logs. -var sensitiveHeaders = map[string]bool{ - "authorization": true, - "www-authenticate": true, - "x-api-key": true, - "api-key": true, - "apikey": true, - "x-api-token": true, - "x-auth-token": true, - "auth-token": true, - "proxy-authenticate": true, - "proxy-authorization": true, - "cookie": true, - "set-cookie": true, - "x-csrf-token": true, - "x-xsrf-token": true, - "x-session-token": true, - "x-access-token": true, -} - -// LoggingHTTPClient is an HTTPClient wrapper that logs HTTP requests and responses. -// -// Logs request method, URL, and headers (with sensitive values redacted) at debug level. -// Logs response status at debug level, and 4xx/5xx responses at error level. -// Does nothing if the logger is silent. -type LoggingHTTPClient struct { - client HTTPClient - logger *Logger -} - -// NewLoggingHTTPClient creates a new LoggingHTTPClient that wraps the given client. -func NewLoggingHTTPClient(client HTTPClient, config *LogConfig) HTTPClient { - if client == nil { - client = &http.Client{} - } - return &LoggingHTTPClient{ - client: client, - logger: config.createLogger(), - } -} - -// Do implements the HTTPClient interface. -func (c *LoggingHTTPClient) Do(req *http.Request) (*http.Response, error) { - // Log the request if debug is enabled - if c.logger.IsDebug() { - c.logRequest(req) - } - - // Perform the request - resp, err := c.client.Do(req) - - // Log the response if debug is enabled - if c.logger.IsDebug() && resp != nil { - c.logResponse(resp) - } - - // Log errors if error logging is enabled - if c.logger.IsError() && err != nil { - c.logger.Error(fmt.Sprintf("HTTP Error: url=%s error=%v", req.URL, err)) - } - - // Log 4xx/5xx responses if error logging is enabled - if c.logger.IsError() && resp != nil && resp.StatusCode >= 400 { - c.logger.Error(fmt.Sprintf("HTTP Error: status=%d url=%s", resp.StatusCode, req.URL)) - } - - return resp, err -} - -// logRequest logs the HTTP request details. -func (c *LoggingHTTPClient) logRequest(req *http.Request) { - var sb strings.Builder - sb.WriteString("HTTP Request: ") - sb.WriteString(req.Method) - sb.WriteString(" ") - sb.WriteString(req.URL.String()) - sb.WriteString(" headers={") - - // Sort header names for consistent output - headerNames := make([]string, 0, len(req.Header)) - for name := range req.Header { - headerNames = append(headerNames, name) - } - sort.Strings(headerNames) - - for i, name := range headerNames { - if i > 0 { - sb.WriteString(", ") - } - sb.WriteString(name) - sb.WriteString("=") - if sensitiveHeaders[strings.ToLower(name)] { - sb.WriteString("[REDACTED]") - } else { - sb.WriteString(strings.Join(req.Header.Values(name), ";")) - } - } - - sb.WriteString("}") - sb.WriteString(" has_body=") - fmt.Fprintf(&sb, "%v", req.Body != nil) - - c.logger.Debug(sb.String()) -} - -// logResponse logs the HTTP response details. -func (c *LoggingHTTPClient) logResponse(resp *http.Response) { - var sb strings.Builder - sb.WriteString("HTTP Response: status=") - fmt.Fprintf(&sb, "%d", resp.StatusCode) - sb.WriteString(" url=") - sb.WriteString(resp.Request.URL.String()) - sb.WriteString(" headers={") - - // Sort header names for consistent output - headerNames := make([]string, 0, len(resp.Header)) - for name := range resp.Header { - headerNames = append(headerNames, name) - } - sort.Strings(headerNames) - - for i, name := range headerNames { - if i > 0 { - sb.WriteString(", ") - } - sb.WriteString(name) - sb.WriteString("=") - if sensitiveHeaders[strings.ToLower(name)] { - sb.WriteString("[REDACTED]") - } else { - sb.WriteString(strings.Join(resp.Header.Values(name), ";")) - } - } - - sb.WriteString("}") - - c.logger.Debug(sb.String()) -} - -// Ensure LoggingHTTPClient implements HTTPClient interface. -var _ HTTPClient = (*LoggingHTTPClient)(nil) diff --git a/seed/go-sdk/imdb/omit-fern-headers/core/request_option.go b/seed/go-sdk/imdb/omit-fern-headers/core/request_option.go index a391b595b692..e605eb600228 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/core/request_option.go +++ b/seed/go-sdk/imdb/omit-fern-headers/core/request_option.go @@ -24,7 +24,6 @@ type RequestOptions struct { QueryParameters url.Values MaxAttempts uint MaxBufSize int - Logging *LogConfig Token string } @@ -121,15 +120,6 @@ func (m *MaxBufSizeOption) applyRequestOptions(opts *RequestOptions) { opts.MaxBufSize = m.MaxBufSize } -// LoggingOption implements the RequestOption interface. -type LoggingOption struct { - Logging *LogConfig -} - -func (l *LoggingOption) applyRequestOptions(opts *RequestOptions) { - opts.Logging = l.Logging -} - // TokenOption implements the RequestOption interface. type TokenOption struct { Token string diff --git a/seed/go-sdk/imdb/omit-fern-headers/go.mod b/seed/go-sdk/imdb/omit-fern-headers/go.mod index 084a422e6764..e0d0b01c7ca3 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/go.mod +++ b/seed/go-sdk/imdb/omit-fern-headers/go.mod @@ -2,7 +2,7 @@ module github.com/imdb/fern go 1.21 -toolchain go1.23.12 +toolchain go1.23.8 require github.com/google/uuid v1.6.0 diff --git a/seed/go-sdk/imdb/omit-fern-headers/option/request_option.go b/seed/go-sdk/imdb/omit-fern-headers/option/request_option.go index 1e8ddaeff7ab..86728c9c3727 100644 --- a/seed/go-sdk/imdb/omit-fern-headers/option/request_option.go +++ b/seed/go-sdk/imdb/omit-fern-headers/option/request_option.go @@ -72,23 +72,6 @@ func WithMaxStreamBufSize(size int) *core.MaxBufSizeOption { } } -// WithLogging configures logging for the SDK. -// By default, logging is silent — no log output unless explicitly configured. -// -// Example: -// -// client := NewClient( -// option.WithLogging(core.NewLogConfigBuilder(). -// Level(core.LogLevelDebug). -// Silent(false). -// Build()), -// ) -func WithLogging(logging *core.LogConfig) *core.LoggingOption { - return &core.LoggingOption{ - Logging: logging, - } -} - // WithToken sets the 'Authorization: Bearer ' request header. func WithToken(token string) *core.TokenOption { return &core.TokenOption{ From 27ac417a68cf19a17509b520f55a05751aaddb69 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Fri, 13 Mar 2026 14:18:13 -0400 Subject: [PATCH 02/29] fix(csharp): Improve gRPC code generation (#13520) --- .../src/asIs/GrpcRequestOptions.Template.cs | 7 +- .../src/proto/CsharpProtobufTypeMapper.ts | 25 +- .../src/endpoint/AbstractEndpointGenerator.ts | 13 +- .../sdk/src/endpoint/EndpointGenerator.ts | 58 +- .../endpoint/grpc/GrpcEndpointGenerator.ts | 11 +- .../endpoint/utils/getEndpointReturnType.ts | 9 +- .../sdk/src/readme/ReadmeSnippetBuilder.ts | 9 +- .../src/root-client/RootClientGenerator.ts | 6 +- .../RootClientInterfaceGenerator.ts | 4 +- .../SubPackageClientGenerator.ts | 6 +- .../SubPackageClientInterfaceGenerator.ts | 4 +- generators/csharp/sdk/versions.yml | 11 + .../baseline-sdks/object-required-allof.json | 84 +- .../__snapshots__/baseline-sdks/readonly.json | 4295 ++ .../type__Column.json | 2 +- .../type__DescribeResponse.json | 2 +- .../type__FetchResponse.json | 2 +- .../type__QueryColumn.json | 4 +- .../type__QueryResponse.json | 2 +- .../type__QueryResult.json | 2 +- .../type__ScoredColumn.json | 2 +- .../type__UpdateResponse.json | 6 +- .../csharp-grpc-proto-exhaustive.json | 4571 ++- .../test-definitions/csharp-grpc-proto.json | 684 +- .../__test__/test-definitions/websocket.json | 131 + .../csharp-grpc-proto-exhaustive.json | 32888 +++++++++++++++- .../test-definitions/csharp-grpc-proto.json | 3905 +- .../__test__/test-definitions/websocket.json | 390 +- .../src/ContainerExecutionEnvironment.ts | 5 + .../src/ExecutionEnvironment.ts | 6 + .../ReusableContainerExecutionEnvironment.ts | 16 +- .../src/runGenerator.ts | 9 + .../proto/data/v1/data.proto | 230 + .../proto/google/api/annotations.proto | 31 + .../proto/google/api/field_behavior.proto | 104 + .../proto/google/api/http.proto | 379 + .../src/SeedApi/Column.cs | 75 + .../src/SeedApi/Core/ProtoAnyMapper.cs | 28 + .../src/SeedApi/Dataservice/IndexType.cs | 67 + .../src/SeedApi/DeleteResponse.cs | 42 + .../src/SeedApi/DescribeResponse.cs | 84 + .../src/SeedApi/FetchResponse.cs | 76 + .../src/SeedApi/FieldBehavior.cs | 95 + .../src/SeedApi/IndexedData.cs | 61 + .../src/SeedApi/ListElement.cs | 50 + .../src/SeedApi/ListResponse.cs | 77 + .../src/SeedApi/Metadata.cs | 39 + .../src/SeedApi/MetadataValue.cs | 91 + .../src/SeedApi/NamespaceSummary.cs | 50 + .../src/SeedApi/Pagination.cs | 50 + .../src/SeedApi/QueryColumn.cs | 86 + .../src/SeedApi/QueryResponse.cs | 77 + .../src/SeedApi/QueryResult.cs | 61 + .../src/SeedApi/ScoredColumn.cs | 83 + .../src/SeedApi/SeedApi.csproj | 33 + .../src/SeedApi/UpdateResponse.cs | 116 + .../src/SeedApi/UploadResponse.cs | 50 + .../src/SeedApi/Usage.cs | 50 + .../proto/google/api/annotations.proto | 31 + .../proto/google/api/http.proto | 379 + .../proto/user/v1/user.proto | 38 + .../src/SeedApi/CreateResponse.cs | 53 + .../csharp-grpc-proto/src/SeedApi/Metadata.cs | 39 + .../src/SeedApi/MetadataValue.cs | 91 + .../src/SeedApi/SeedApi.csproj | 28 + .../src/SeedApi/UserModel.cs | 85 + .../include-exception-handler/README.md | 66 +- .../proto/data/v1/data.proto | 230 + .../proto/google/api/annotations.proto | 31 + .../proto/google/api/field_behavior.proto | 104 + .../proto/google/api/http.proto | 379 + .../include-exception-handler/reference.md | 294 +- .../include-exception-handler/snippet.json | 84 + .../src/SeedApi.DynamicSnippets/Example0.cs | 17 - .../src/SeedApi.DynamicSnippets/Example1.cs | 17 - .../SeedApi.DynamicSnippets.csproj | 13 - .../Unit/MockServer/BaseMockServerTest.cs | 38 - .../Unit/MockServer/Dataservice/FooTest.cs | 55 - .../src/SeedApi/Core/ProtoAnyMapper.cs | 28 + .../src/SeedApi/Core/Public/ClientOptions.cs | 12 + .../SeedApi/Core/Public/GrpcRequestOptions.cs | 63 + .../src/SeedApi/Core/RawClient.cs | 7 + .../src/SeedApi/Core/RawGrpcClient.cs | 61 + .../SeedApi/Dataservice/DataserviceClient.cs | 461 + .../SeedApi/Dataservice/IDataserviceClient.cs | 42 + .../Dataservice/Requests/DeleteRequest.cs | 52 + .../Dataservice/Requests/DescribeRequest.cs | 39 + .../Dataservice/Requests/FetchRequest.cs | 38 + .../Dataservice/Requests/ListRequest.cs | 52 + .../Dataservice/Requests/QueryRequest.cs | 84 + .../Dataservice/Requests/UpdateRequest.cs | 100 + .../Dataservice/Requests/UploadRequest.cs | 38 + .../SeedApi/Dataservice/Types/IndexType.cs | 67 + .../src/SeedApi/SeedApi.csproj | 33 + .../src/SeedApi/Types/Column.cs | 75 + .../src/SeedApi/Types/DeleteResponse.cs | 42 + .../src/SeedApi/Types/DescribeResponse.cs | 84 + .../src/SeedApi/Types/FetchResponse.cs | 76 + .../src/SeedApi/Types/FieldBehavior.cs | 95 + .../src/SeedApi/Types/IndexedData.cs | 61 + .../src/SeedApi/Types/ListElement.cs | 50 + .../src/SeedApi/Types/ListResponse.cs | 77 + .../src/SeedApi/Types/Metadata.cs | 39 + .../src/SeedApi/Types/MetadataValue.cs | 91 + .../src/SeedApi/Types/NamespaceSummary.cs | 50 + .../src/SeedApi/Types/Pagination.cs | 50 + .../src/SeedApi/Types/QueryColumn.cs | 86 + .../src/SeedApi/Types/QueryResponse.cs | 77 + .../src/SeedApi/Types/QueryResult.cs | 61 + .../src/SeedApi/Types/ScoredColumn.cs | 83 + .../src/SeedApi/Types/UpdateResponse.cs | 116 + .../src/SeedApi/Types/UploadResponse.cs | 50 + .../src/SeedApi/Types/Usage.cs | 50 + .../no-custom-config/README.md | 66 +- .../no-custom-config/proto/data/v1/data.proto | 230 + .../proto/google/api/annotations.proto | 31 + .../proto/google/api/field_behavior.proto | 104 + .../proto/google/api/http.proto | 379 + .../no-custom-config/reference.md | 294 +- .../no-custom-config/snippet.json | 84 + .../src/SeedApi.DynamicSnippets/Example0.cs | 17 - .../src/SeedApi.DynamicSnippets/Example1.cs | 17 - .../SeedApi.DynamicSnippets.csproj | 13 - .../Unit/MockServer/BaseMockServerTest.cs | 38 - .../Unit/MockServer/Dataservice/FooTest.cs | 55 - .../src/SeedApi/Core/ProtoAnyMapper.cs | 28 + .../src/SeedApi/Core/Public/ClientOptions.cs | 12 + .../SeedApi/Core/Public/GrpcRequestOptions.cs | 63 + .../src/SeedApi/Core/RawClient.cs | 7 + .../src/SeedApi/Core/RawGrpcClient.cs | 61 + .../SeedApi/Dataservice/DataserviceClient.cs | 426 + .../SeedApi/Dataservice/IDataserviceClient.cs | 42 + .../Dataservice/Requests/DeleteRequest.cs | 52 + .../Dataservice/Requests/DescribeRequest.cs | 39 + .../Dataservice/Requests/FetchRequest.cs | 38 + .../Dataservice/Requests/ListRequest.cs | 52 + .../Dataservice/Requests/QueryRequest.cs | 84 + .../Dataservice/Requests/UpdateRequest.cs | 100 + .../Dataservice/Requests/UploadRequest.cs | 38 + .../SeedApi/Dataservice/Types/IndexType.cs | 67 + .../src/SeedApi/SeedApi.csproj | 33 + .../src/SeedApi/Types/Column.cs | 75 + .../src/SeedApi/Types/DeleteResponse.cs | 42 + .../src/SeedApi/Types/DescribeResponse.cs | 84 + .../src/SeedApi/Types/FetchResponse.cs | 76 + .../src/SeedApi/Types/FieldBehavior.cs | 95 + .../src/SeedApi/Types/IndexedData.cs | 61 + .../src/SeedApi/Types/ListElement.cs | 50 + .../src/SeedApi/Types/ListResponse.cs | 77 + .../src/SeedApi/Types/Metadata.cs | 39 + .../src/SeedApi/Types/MetadataValue.cs | 91 + .../src/SeedApi/Types/NamespaceSummary.cs | 50 + .../src/SeedApi/Types/Pagination.cs | 50 + .../src/SeedApi/Types/QueryColumn.cs | 86 + .../src/SeedApi/Types/QueryResponse.cs | 77 + .../src/SeedApi/Types/QueryResult.cs | 61 + .../src/SeedApi/Types/ScoredColumn.cs | 83 + .../src/SeedApi/Types/UpdateResponse.cs | 116 + .../src/SeedApi/Types/UploadResponse.cs | 50 + .../src/SeedApi/Types/Usage.cs | 50 + .../package-id/README.md | 66 +- .../package-id/proto/data/v1/data.proto | 230 + .../proto/google/api/annotations.proto | 31 + .../proto/google/api/field_behavior.proto | 104 + .../package-id/proto/google/api/http.proto | 379 + .../package-id/reference.md | 294 +- .../package-id/snippet.json | 84 + .../src/SeedApi.DynamicSnippets/Example0.cs | 17 - .../src/SeedApi.DynamicSnippets/Example1.cs | 17 - .../SeedApi.DynamicSnippets.csproj | 13 - .../Unit/MockServer/BaseMockServerTest.cs | 38 - .../Unit/MockServer/Dataservice/FooTest.cs | 55 - .../src/SeedApi/Core/ProtoAnyMapper.cs | 28 + .../src/SeedApi/Core/Public/ClientOptions.cs | 12 + .../SeedApi/Core/Public/GrpcRequestOptions.cs | 63 + .../package-id/src/SeedApi/Core/RawClient.cs | 7 + .../src/SeedApi/Core/RawGrpcClient.cs | 61 + .../SeedApi/Dataservice/DataserviceClient.cs | 426 + .../SeedApi/Dataservice/IDataserviceClient.cs | 42 + .../Dataservice/Requests/DeleteRequest.cs | 52 + .../Dataservice/Requests/DescribeRequest.cs | 39 + .../Dataservice/Requests/FetchRequest.cs | 38 + .../Dataservice/Requests/ListRequest.cs | 52 + .../Dataservice/Requests/QueryRequest.cs | 84 + .../Dataservice/Requests/UpdateRequest.cs | 100 + .../Dataservice/Requests/UploadRequest.cs | 38 + .../SeedApi/Dataservice/Types/IndexType.cs | 67 + .../package-id/src/SeedApi/SeedApi.csproj | 33 + .../package-id/src/SeedApi/Types/Column.cs | 75 + .../src/SeedApi/Types/DeleteResponse.cs | 42 + .../src/SeedApi/Types/DescribeResponse.cs | 84 + .../src/SeedApi/Types/FetchResponse.cs | 76 + .../src/SeedApi/Types/FieldBehavior.cs | 95 + .../src/SeedApi/Types/IndexedData.cs | 61 + .../src/SeedApi/Types/ListElement.cs | 50 + .../src/SeedApi/Types/ListResponse.cs | 77 + .../package-id/src/SeedApi/Types/Metadata.cs | 39 + .../src/SeedApi/Types/MetadataValue.cs | 91 + .../src/SeedApi/Types/NamespaceSummary.cs | 50 + .../src/SeedApi/Types/Pagination.cs | 50 + .../src/SeedApi/Types/QueryColumn.cs | 86 + .../src/SeedApi/Types/QueryResponse.cs | 77 + .../src/SeedApi/Types/QueryResult.cs | 61 + .../src/SeedApi/Types/ScoredColumn.cs | 83 + .../src/SeedApi/Types/UpdateResponse.cs | 116 + .../src/SeedApi/Types/UploadResponse.cs | 50 + .../package-id/src/SeedApi/Types/Usage.cs | 50 + .../read-only-memory/README.md | 66 +- .../read-only-memory/proto/data/v1/data.proto | 230 + .../proto/google/api/annotations.proto | 31 + .../proto/google/api/field_behavior.proto | 104 + .../proto/google/api/http.proto | 379 + .../read-only-memory/reference.md | 290 +- .../read-only-memory/snippet.json | 84 + .../src/SeedApi.DynamicSnippets/Example0.cs | 17 - .../src/SeedApi.DynamicSnippets/Example1.cs | 17 - .../SeedApi.DynamicSnippets.csproj | 13 - .../Unit/MockServer/BaseMockServerTest.cs | 38 - .../Unit/MockServer/Dataservice/FooTest.cs | 55 - .../src/SeedApi/Core/ProtoAnyMapper.cs | 28 + .../src/SeedApi/Core/Public/ClientOptions.cs | 12 + .../SeedApi/Core/Public/GrpcRequestOptions.cs | 63 + .../src/SeedApi/Core/RawClient.cs | 7 + .../src/SeedApi/Core/RawGrpcClient.cs | 61 + .../SeedApi/Dataservice/DataserviceClient.cs | 422 + .../SeedApi/Dataservice/IDataserviceClient.cs | 42 + .../Dataservice/Requests/DeleteRequest.cs | 52 + .../Dataservice/Requests/DescribeRequest.cs | 39 + .../Dataservice/Requests/FetchRequest.cs | 38 + .../Dataservice/Requests/ListRequest.cs | 52 + .../Dataservice/Requests/QueryRequest.cs | 84 + .../Dataservice/Requests/UpdateRequest.cs | 100 + .../Dataservice/Requests/UploadRequest.cs | 38 + .../SeedApi/Dataservice/Types/IndexType.cs | 67 + .../src/SeedApi/SeedApi.csproj | 33 + .../src/SeedApi/Types/Column.cs | 75 + .../src/SeedApi/Types/DeleteResponse.cs | 42 + .../src/SeedApi/Types/DescribeResponse.cs | 84 + .../src/SeedApi/Types/FetchResponse.cs | 76 + .../src/SeedApi/Types/FieldBehavior.cs | 95 + .../src/SeedApi/Types/IndexedData.cs | 61 + .../src/SeedApi/Types/ListElement.cs | 50 + .../src/SeedApi/Types/ListResponse.cs | 77 + .../src/SeedApi/Types/Metadata.cs | 39 + .../src/SeedApi/Types/MetadataValue.cs | 91 + .../src/SeedApi/Types/NamespaceSummary.cs | 50 + .../src/SeedApi/Types/Pagination.cs | 50 + .../src/SeedApi/Types/QueryColumn.cs | 86 + .../src/SeedApi/Types/QueryResponse.cs | 77 + .../src/SeedApi/Types/QueryResult.cs | 61 + .../src/SeedApi/Types/ScoredColumn.cs | 83 + .../src/SeedApi/Types/UpdateResponse.cs | 116 + .../src/SeedApi/Types/UploadResponse.cs | 50 + .../src/SeedApi/Types/Usage.cs | 50 + .../{ => no-custom-config}/.editorconfig | 0 .../.fern/metadata.json | 0 .../.github/workflows/ci.yml | 0 .../{ => no-custom-config}/.gitignore | 0 .../no-custom-config/README.md | 124 + .../{ => no-custom-config}/SeedApi.slnx | 0 .../proto/google/api/annotations.proto | 31 + .../proto/google/api/http.proto | 379 + .../no-custom-config/proto/user/v1/user.proto | 38 + .../no-custom-config/reference.md | 42 + .../no-custom-config/snippet.json | 17 + .../SeedApi.Test/Core/HeadersBuilderTests.cs | 0 .../Core/Json/AdditionalPropertiesTests.cs | 0 .../Core/Json/DateOnlyJsonTests.cs | 0 .../Core/Json/DateTimeJsonTests.cs | 0 .../Core/Json/JsonAccessAttributeTests.cs | 0 .../Core/Json/StringEnumSerializerTests.cs | 0 .../Core/QueryStringBuilderTests.cs | 0 .../Core/QueryStringConverterTests.cs | 0 .../Core/RawClientTests/MultipartFormTests.cs | 0 .../RawClientTests/QueryParameterTests.cs | 0 .../Core/RawClientTests/RetriesTests.cs | 0 .../SeedApi.Test/Core/WithRawResponseTests.cs | 0 .../SeedApi.Test/SeedApi.Test.Custom.props | 0 .../src/SeedApi.Test/SeedApi.Test.csproj | 0 .../src/SeedApi.Test/TestClient.cs | 0 .../Utils/AdditionalPropertiesComparer.cs | 0 .../src/SeedApi.Test/Utils/JsonAssert.cs | 0 .../SeedApi.Test/Utils/JsonElementComparer.cs | 0 .../src/SeedApi.Test/Utils/NUnitExtensions.cs | 0 .../src/SeedApi.Test/Utils/OneOfComparer.cs | 0 .../SeedApi.Test/Utils/OptionalComparer.cs | 0 .../Utils/ReadOnlyMemoryComparer.cs | 0 .../src/SeedApi/Core/ApiResponse.cs | 0 .../src/SeedApi/Core/BaseRequest.cs | 0 .../SeedApi/Core/CollectionItemSerializer.cs | 0 .../src/SeedApi/Core/Constants.cs | 0 .../src/SeedApi/Core/DateOnlyConverter.cs | 0 .../src/SeedApi/Core/DateTimeSerializer.cs | 0 .../src/SeedApi/Core/EmptyRequest.cs | 0 .../src/SeedApi/Core/EncodingCache.cs | 0 .../src/SeedApi/Core/Extensions.cs | 0 .../src/SeedApi/Core/FormUrlEncoder.cs | 0 .../src/SeedApi/Core/HeaderValue.cs | 0 .../src/SeedApi/Core/Headers.cs | 0 .../src/SeedApi/Core/HeadersBuilder.cs | 0 .../src/SeedApi/Core/HttpContentExtensions.cs | 0 .../src/SeedApi/Core/HttpMethodExtensions.cs | 0 .../src/SeedApi/Core/IIsRetryableContent.cs | 0 .../src/SeedApi/Core/IRequestOptions.cs | 0 .../src/SeedApi/Core/JsonAccessAttribute.cs | 0 .../src/SeedApi/Core/JsonConfiguration.cs | 0 .../src/SeedApi/Core/JsonRequest.cs | 0 .../src/SeedApi/Core/MultipartFormRequest.cs | 0 .../src/SeedApi/Core/NullableAttribute.cs | 0 .../src/SeedApi/Core/OneOfSerializer.cs | 0 .../src/SeedApi/Core/Optional.cs | 0 .../src/SeedApi/Core/OptionalAttribute.cs | 0 .../Core/Public/AdditionalProperties.cs | 0 .../src/SeedApi/Core/Public/ClientOptions.cs | 12 + .../src/SeedApi/Core/Public/FileParameter.cs | 0 .../SeedApi/Core/Public/GrpcRequestOptions.cs | 63 + .../src/SeedApi/Core/Public/RawResponse.cs | 0 .../src/SeedApi/Core/Public/RequestOptions.cs | 0 .../Core/Public/SeedApiApiException.cs | 0 .../SeedApi/Core/Public/SeedApiException.cs | 0 .../src/SeedApi/Core/Public/Version.cs | 0 .../SeedApi/Core/Public/WithRawResponse.cs | 0 .../Core/Public/WithRawResponseTask.cs | 0 .../src/SeedApi/Core/QueryStringBuilder.cs | 0 .../src/SeedApi/Core/QueryStringConverter.cs | 0 .../src/SeedApi/Core/RawClient.cs | 7 + .../src/SeedApi/Core/RawGrpcClient.cs | 61 + .../src/SeedApi/Core/RawResponse.cs | 0 .../src/SeedApi/Core/ResponseHeaders.cs | 0 .../src/SeedApi/Core/StreamRequest.cs | 0 .../src/SeedApi/Core/StringEnum.cs | 0 .../src/SeedApi/Core/StringEnumExtensions.cs | 0 .../src/SeedApi/Core/StringEnumSerializer.cs | 0 .../src/SeedApi/Core/ValueConvert.cs | 0 .../src/SeedApi/ISeedApiClient.cs | 6 + .../src/SeedApi/SeedApi.Custom.props | 0 .../src/SeedApi/SeedApi.csproj | 28 + .../src/SeedApi/SeedApiClient.cs | 3 + .../src/SeedApi/Types/CreateResponse.cs | 53 + .../src/SeedApi/Types/Metadata.cs | 39 + .../src/SeedApi/Types/MetadataValue.cs | 91 + .../src/SeedApi/Types/UserModel.cs | 85 + .../SeedApi/Userservice/IUserserviceClient.cs | 10 + .../Userservice/Requests/CreateRequest.cs | 59 + .../SeedApi/Userservice/UserserviceClient.cs | 79 + .../csharp-sdk/csharp-grpc-proto/snippet.json | 4 - .../SeedApi.DynamicSnippets.csproj | 13 - .../src/SeedApi/ISeedApiClient.cs | 3 - seed/csharp-sdk/seed.yml | 13 +- .../generators.yml | 1 + .../overrides.yml | 12 +- .../apis/csharp-grpc-proto/generators.yml | 1 + 352 files changed, 65315 insertions(+), 1216 deletions(-) create mode 100644 packages/cli/api-importers/v3-importer-tests/src/__test__/__snapshots__/baseline-sdks/readonly.json create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/proto/data/v1/data.proto create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/annotations.proto create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/field_behavior.proto create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/http.proto create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Column.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/ProtoAnyMapper.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DeleteResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DescribeResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FetchResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/IndexedData.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListElement.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Metadata.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/MetadataValue.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/NamespaceSummary.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Pagination.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryColumn.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResult.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ScoredColumn.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UpdateResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UploadResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Usage.cs create mode 100644 seed/csharp-model/csharp-grpc-proto/proto/google/api/annotations.proto create mode 100644 seed/csharp-model/csharp-grpc-proto/proto/google/api/http.proto create mode 100644 seed/csharp-model/csharp-grpc-proto/proto/user/v1/user.proto create mode 100644 seed/csharp-model/csharp-grpc-proto/src/SeedApi/CreateResponse.cs create mode 100644 seed/csharp-model/csharp-grpc-proto/src/SeedApi/Metadata.cs create mode 100644 seed/csharp-model/csharp-grpc-proto/src/SeedApi/MetadataValue.cs create mode 100644 seed/csharp-model/csharp-grpc-proto/src/SeedApi/UserModel.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/data/v1/data.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/annotations.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/field_behavior.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/http.proto delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example0.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example1.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/ProtoAnyMapper.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/GrpcRequestOptions.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawGrpcClient.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DeleteRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DescribeRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/FetchRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/ListRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/QueryRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UpdateRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UploadRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Column.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DeleteResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DescribeResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FetchResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/IndexedData.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListElement.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Metadata.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/MetadataValue.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/NamespaceSummary.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Pagination.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResult.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ScoredColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UpdateResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UploadResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Usage.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/data/v1/data.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/annotations.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/field_behavior.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/http.proto delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example0.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example1.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/ProtoAnyMapper.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DeleteRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DescribeRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/FetchRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/ListRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/QueryRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UpdateRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UploadRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Column.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DeleteResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DescribeResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FetchResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/IndexedData.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListElement.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Metadata.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/MetadataValue.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/NamespaceSummary.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Pagination.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResult.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ScoredColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UpdateResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UploadResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Usage.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/data/v1/data.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/annotations.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/field_behavior.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/http.proto delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example0.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example1.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/ProtoAnyMapper.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/GrpcRequestOptions.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawGrpcClient.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DeleteRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DescribeRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/FetchRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/ListRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/QueryRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UpdateRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UploadRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Column.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DeleteResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DescribeResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FetchResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/IndexedData.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListElement.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Metadata.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/MetadataValue.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/NamespaceSummary.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Pagination.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResult.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ScoredColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UpdateResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UploadResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Usage.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/data/v1/data.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/annotations.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/field_behavior.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/http.proto delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example0.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example1.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/ProtoAnyMapper.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/GrpcRequestOptions.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawGrpcClient.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DeleteRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DescribeRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/FetchRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/ListRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/QueryRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UpdateRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UploadRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Column.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DeleteResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DescribeResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FetchResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/IndexedData.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListElement.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Metadata.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/MetadataValue.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/NamespaceSummary.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Pagination.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResult.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ScoredColumn.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UpdateResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UploadResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Usage.cs rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/.editorconfig (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/.fern/metadata.json (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/.github/workflows/ci.yml (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/.gitignore (100%) create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/README.md rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/SeedApi.slnx (100%) create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/annotations.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/http.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/user/v1/user.proto create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/reference.md create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/snippet.json rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/HeadersBuilderTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/Json/AdditionalPropertiesTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/Json/DateOnlyJsonTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/Json/DateTimeJsonTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/QueryStringBuilderTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/QueryStringConverterTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/RawClientTests/MultipartFormTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/RawClientTests/QueryParameterTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Core/WithRawResponseTests.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/SeedApi.Test.Custom.props (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/SeedApi.Test.csproj (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/TestClient.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Utils/AdditionalPropertiesComparer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Utils/JsonAssert.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Utils/JsonElementComparer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Utils/NUnitExtensions.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Utils/OneOfComparer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Utils/OptionalComparer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi.Test/Utils/ReadOnlyMemoryComparer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/ApiResponse.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/BaseRequest.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/CollectionItemSerializer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Constants.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/DateOnlyConverter.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/DateTimeSerializer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/EmptyRequest.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/EncodingCache.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Extensions.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/FormUrlEncoder.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/HeaderValue.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Headers.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/HeadersBuilder.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/HttpContentExtensions.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/HttpMethodExtensions.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/IIsRetryableContent.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/IRequestOptions.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/JsonAccessAttribute.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/JsonConfiguration.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/JsonRequest.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/MultipartFormRequest.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/NullableAttribute.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/OneOfSerializer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Optional.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/OptionalAttribute.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/AdditionalProperties.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/ClientOptions.cs (88%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/FileParameter.cs (100%) create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/RawResponse.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/RequestOptions.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/SeedApiApiException.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/SeedApiException.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/Version.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/WithRawResponse.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/Public/WithRawResponseTask.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/QueryStringBuilder.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/QueryStringConverter.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/RawClient.cs (98%) create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/RawResponse.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/ResponseHeaders.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/StreamRequest.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/StringEnum.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/StringEnumExtensions.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/StringEnumSerializer.cs (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/Core/ValueConvert.cs (100%) create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/ISeedApiClient.cs rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/SeedApi.Custom.props (100%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/SeedApi.csproj (73%) rename seed/csharp-sdk/csharp-grpc-proto/{ => no-custom-config}/src/SeedApi/SeedApiClient.cs (89%) create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/CreateResponse.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/Metadata.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/MetadataValue.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/UserModel.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/IUserserviceClient.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/Requests/CreateRequest.cs create mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/UserserviceClient.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto/snippet.json delete mode 100644 seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj delete mode 100644 seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/ISeedApiClient.cs diff --git a/generators/csharp/base/src/asIs/GrpcRequestOptions.Template.cs b/generators/csharp/base/src/asIs/GrpcRequestOptions.Template.cs index 0103fc997dd8..45872a84d637 100644 --- a/generators/csharp/base/src/asIs/GrpcRequestOptions.Template.cs +++ b/generators/csharp/base/src/asIs/GrpcRequestOptions.Template.cs @@ -54,14 +54,15 @@ public CallCredentials? CallCredentials { } /// - /// Headers to be sent with this particular request. + /// Additional headers to be sent with this particular request. + /// Headers with matching keys will be overwritten by headers set on the client options. /// - internal Headers Headers { + public IEnumerable> AdditionalHeaders { get; #if NET5_0_OR_GREATER init; #else set; #endif - } = new(); + } = new List>(); } diff --git a/generators/csharp/base/src/proto/CsharpProtobufTypeMapper.ts b/generators/csharp/base/src/proto/CsharpProtobufTypeMapper.ts index a97825753e87..4902d298dfd5 100644 --- a/generators/csharp/base/src/proto/CsharpProtobufTypeMapper.ts +++ b/generators/csharp/base/src/proto/CsharpProtobufTypeMapper.ts @@ -320,7 +320,8 @@ class ToProtoPropertyMapper extends WithGeneration { enum_: resolvedType.shape, classReference: enumClassReference, protobufClassReference, - propertyName + propertyName, + wrapperType }); } if (wrapperType === WrapperType.List) { @@ -361,11 +362,13 @@ class ToProtoPropertyMapper extends WithGeneration { propertyName: string; }): ast.CodeBlock { return this.csharp.codeblock((writer) => { - writer.writeLine(`${propertyName}.Select(type => type switch`); + // Switch on the string Value property using const string patterns from Values class, + // since the enum is a readonly record struct (static readonly fields aren't constant patterns). + writer.writeLine(`${propertyName}.Select(type => type.Value switch`); writer.writeLine("{"); for (const enumValue of enum_.values) { writer.writeNode(classReference); - writer.write("."); + writer.write(".Values."); writer.write(enumValue.name.name.pascalCase.safeName); writer.write(" => "); writer.writeNode(protobufClassReference); @@ -382,19 +385,25 @@ class ToProtoPropertyMapper extends WithGeneration { enum_, classReference, protobufClassReference, - propertyName + propertyName, + wrapperType }: { enum_: EnumTypeDeclaration; classReference: ast.ClassReference; protobufClassReference: ast.ClassReference; propertyName: string; + wrapperType?: WrapperType; }): ast.CodeBlock { return this.csharp.codeblock((writer) => { - writer.writeLine(`${propertyName}.Value switch`); + // Switch on the string Value property using const string patterns from Values class, + // since the enum is a readonly record struct (static readonly fields aren't constant patterns). + // For optional enums, .Value does the nullable unwrap, so we need .Value.Value to get the string. + const valueAccess = wrapperType === WrapperType.Optional ? ".Value.Value" : ".Value"; + writer.writeLine(`${propertyName}${valueAccess} switch`); writer.writeLine("{"); for (const enumValue of enum_.values) { writer.writeNode(classReference); - writer.write("."); + writer.write(".Values."); writer.write(enumValue.name.name.pascalCase.safeName); writer.write(" => "); writer.writeNode(protobufClassReference); @@ -402,7 +411,9 @@ class ToProtoPropertyMapper extends WithGeneration { writer.write(getProtobufEnumValueName({ generation: this.generation, classReference, enumValue })); writer.writeLine(","); } - writer.writeLine(` _ => throw new ArgumentException($"Unknown enum value: {${propertyName}.Value}")`); + writer.writeLine( + ` _ => throw new ArgumentException($"Unknown enum value: {${propertyName}${valueAccess}}")` + ); writer.write("}"); }); } diff --git a/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts b/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts index e4c8afc8006c..e29b2bdb2e0a 100644 --- a/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts @@ -42,15 +42,18 @@ export abstract class AbstractEndpointGenerator extends WithGeneration { protected getUnpagedEndpointSignatureInfo({ serviceId, - endpoint + endpoint, + isGrpc }: { serviceId: ServiceId; endpoint: HttpEndpoint; + isGrpc?: boolean; }): EndpointSignatureInfo { return this.getEndpointSignatureInfoFor({ serviceId, endpoint, - endpointType: "unpaged" + endpointType: "unpaged", + isGrpc }); } @@ -71,11 +74,13 @@ export abstract class AbstractEndpointGenerator extends WithGeneration { protected getEndpointSignatureInfoFor({ serviceId, endpoint, - endpointType + endpointType, + isGrpc }: { serviceId: ServiceId; endpoint: HttpEndpoint; endpointType: "unpaged" | "paged"; + isGrpc?: boolean; }): EndpointSignatureInfo { const request = getEndpointRequest({ context: this.context, @@ -96,7 +101,7 @@ export abstract class AbstractEndpointGenerator extends WithGeneration { let returnType: ast.Type | undefined; switch (endpointType) { case "unpaged": - returnType = getEndpointReturnType({ context: this.context, endpoint }); + returnType = getEndpointReturnType({ context: this.context, endpoint, isGrpc }); break; case "paged": returnType = this.getPagerReturnType(endpoint); diff --git a/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts b/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts index 18fdd905101e..9c1cbe391c86 100644 --- a/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts @@ -27,21 +27,28 @@ export class EndpointGenerator extends AbstractEndpointGenerator { interface_: ast.Interface, { serviceId, - endpoint + endpoint, + grpcClientInfo }: { serviceId: ServiceId; endpoint: HttpEndpoint; + grpcClientInfo?: GrpcClientInfo; } ): void { if (this.hasPagination(endpoint)) { switch (endpoint.pagination.type) { case "offset": case "cursor": - this.generatePagerInterfaceSignature(interface_, { serviceId, endpoint }); - this.generateUnpagedInterfaceSignature(interface_, { serviceId, endpoint, isPrivate: true }); + this.generatePagerInterfaceSignature(interface_, { serviceId, endpoint, grpcClientInfo }); + this.generateUnpagedInterfaceSignature(interface_, { + serviceId, + endpoint, + isPrivate: true, + grpcClientInfo + }); break; case "custom": - this.generatePagerInterfaceSignature(interface_, { serviceId, endpoint }); + this.generatePagerInterfaceSignature(interface_, { serviceId, endpoint, grpcClientInfo }); break; case "uri": case "path": @@ -53,7 +60,12 @@ export class EndpointGenerator extends AbstractEndpointGenerator { assertNever(endpoint.pagination); } } else { - this.generateUnpagedInterfaceSignature(interface_, { serviceId, endpoint, isPrivate: false }); + this.generateUnpagedInterfaceSignature(interface_, { + serviceId, + endpoint, + isPrivate: false, + grpcClientInfo + }); } } @@ -62,11 +74,13 @@ export class EndpointGenerator extends AbstractEndpointGenerator { { serviceId, endpoint, - isPrivate + isPrivate, + grpcClientInfo }: { serviceId: ServiceId; endpoint: HttpEndpoint; isPrivate: boolean; + grpcClientInfo?: GrpcClientInfo; } ): void { if (isPrivate) { @@ -77,7 +91,7 @@ export class EndpointGenerator extends AbstractEndpointGenerator { endpoint }); const parameters = [...endpointSignatureInfo.baseParameters]; - parameters.push(this.getRequestOptionsParameter({ endpoint })); + parameters.push(this.getRequestOptionsParameter({ endpoint, grpcClientInfo })); parameters.push( this.csharp.parameter({ type: this.System.Threading.CancellationToken, @@ -85,7 +99,8 @@ export class EndpointGenerator extends AbstractEndpointGenerator { initializer: "default" }) ); - const rawReturn = getEndpointReturnType({ context: this.context, endpoint }); + const isGrpc = this.isGrpcEndpoint(grpcClientInfo, endpoint); + const rawReturn = getEndpointReturnType({ context: this.context, endpoint, isGrpc }); // Check if this is a streaming endpoint (returns IAsyncEnumerable) // Streaming endpoints use async iterators which return IAsyncEnumerable directly, not Task> @@ -107,6 +122,7 @@ export class EndpointGenerator extends AbstractEndpointGenerator { // For interface methods: // - Streaming endpoints return IAsyncEnumerable directly (async iterator pattern) // - WithRawResponseTask is already task-like, don't wrap in Task<> + // - gRPC endpoints return Task (no raw response support) // - Empty responses return Task let return_: ast.Type; if (isStreaming) { @@ -116,8 +132,8 @@ export class EndpointGenerator extends AbstractEndpointGenerator { // WithRawResponseTask is already task-like, use it directly return_ = rawReturn; } else if (rawReturn != null) { - // Other non-streaming endpoints (like HEAD requests that return HttpResponseHeaders) - return_ = rawReturn; + // gRPC and other non-streaming endpoints: wrap in Task + return_ = this.System.Threading.Tasks.Task(rawReturn); } else { // Empty responses return Task return_ = this.System.Threading.Tasks.Task(); @@ -136,10 +152,12 @@ export class EndpointGenerator extends AbstractEndpointGenerator { interface_: ast.Interface, { serviceId, - endpoint + endpoint, + grpcClientInfo }: { serviceId: ServiceId; endpoint: HttpEndpoint; + grpcClientInfo?: GrpcClientInfo; } ): void { const endpointSignatureInfo = this.getEndpointSignatureInfo({ @@ -147,7 +165,7 @@ export class EndpointGenerator extends AbstractEndpointGenerator { endpoint }); const parameters = [...endpointSignatureInfo.baseParameters]; - parameters.push(this.getRequestOptionsParameter({ endpoint })); + parameters.push(this.getRequestOptionsParameter({ endpoint, grpcClientInfo })); parameters.push( this.csharp.parameter({ type: this.System.Threading.CancellationToken, @@ -168,7 +186,21 @@ export class EndpointGenerator extends AbstractEndpointGenerator { }); } - private getRequestOptionsParameter({ endpoint }: { endpoint: HttpEndpoint }): ast.Parameter { + private getRequestOptionsParameter({ + endpoint, + grpcClientInfo + }: { + endpoint: HttpEndpoint; + grpcClientInfo?: GrpcClientInfo; + }): ast.Parameter { + const isGrpc = this.isGrpcEndpoint(grpcClientInfo, endpoint); + if (isGrpc) { + return this.csharp.parameter({ + type: this.Types.GrpcRequestOptions.asOptional(), + name: this.names.parameters.requestOptions, + initializer: "null" + }); + } const isIdempotent = endpoint.idempotent; // Use concrete RequestOptions/IdempotentRequestOptions classes (public) instead of interfaces (internal) // to ensure interface methods have consistent accessibility diff --git a/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts b/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts index 58b5fb7d74bc..5decad8049f3 100644 --- a/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts @@ -150,14 +150,15 @@ export class GrpcEndpointGenerator extends AbstractEndpointGenerator { // Build gRPC metadata from headers writer.writeLine("var metadata = new global::Grpc.Core.Metadata();"); - // Add client-level headers (includes lazy auth headers) + // Add client-level headers (includes lazy auth headers). + // HeaderValue requires async resolution via ResolveAsync(). writer.writeLine("foreach (var header in _client.Options.Headers)"); writer.pushScope(); - writer.writeLine("var value = header.Value?.Match(str => str, func => func.Invoke());"); - writer.writeLine("if (value != null) metadata.Add(header.Key, value);"); + writer.writeLine("var value = await header.Value.ResolveAsync().ConfigureAwait(false);"); + writer.writeLine("metadata.Add(header.Key, value);"); writer.popScope(); - // Add client-level additional headers + // Add client-level additional headers (string-based) writer.writeLine("if (_client.Options.AdditionalHeaders != null)"); writer.pushScope(); writer.writeLine("foreach (var header in _client.Options.AdditionalHeaders)"); @@ -295,6 +296,6 @@ export class GrpcEndpointGenerator extends AbstractEndpointGenerator { serviceId: string; endpoint: HttpEndpoint; }): EndpointSignatureInfo { - return super.getUnpagedEndpointSignatureInfo({ serviceId, endpoint }); + return super.getUnpagedEndpointSignatureInfo({ serviceId, endpoint, isGrpc: true }); } } diff --git a/generators/csharp/sdk/src/endpoint/utils/getEndpointReturnType.ts b/generators/csharp/sdk/src/endpoint/utils/getEndpointReturnType.ts index ac5f873545b5..24432cd0ad83 100644 --- a/generators/csharp/sdk/src/endpoint/utils/getEndpointReturnType.ts +++ b/generators/csharp/sdk/src/endpoint/utils/getEndpointReturnType.ts @@ -19,10 +19,12 @@ function wrapWithRawResponseTask(context: SdkGeneratorContext, innerType: ast.Ty export function getEndpointReturnType({ context, - endpoint + endpoint, + isGrpc }: { context: SdkGeneratorContext; endpoint: HttpEndpoint; + isGrpc?: boolean; }): ast.Type | undefined { if (endpoint.response?.body == null) { if (endpoint.method === FernIr.HttpMethod.Head) { @@ -60,8 +62,9 @@ export function getEndpointReturnType({ _other: () => undefined }); - // Wrap non-streaming responses in WithRawResponseTask - if (baseType != null && !isStreamingEndpoint(endpoint)) { + // Wrap non-streaming, non-gRPC responses in WithRawResponseTask. + // gRPC endpoints don't support response headers/trailers, so they return Task directly. + if (baseType != null && !isStreamingEndpoint(endpoint) && !isGrpc) { return wrapWithRawResponseTask(context, baseType); } diff --git a/generators/csharp/sdk/src/readme/ReadmeSnippetBuilder.ts b/generators/csharp/sdk/src/readme/ReadmeSnippetBuilder.ts index 1f8456812d84..e44b488230be 100644 --- a/generators/csharp/sdk/src/readme/ReadmeSnippetBuilder.ts +++ b/generators/csharp/sdk/src/readme/ReadmeSnippetBuilder.ts @@ -114,10 +114,13 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder { snippets[FernGeneratorCli.StructuredFeatureId.Retries] = this.buildRetrySnippets(); snippets[FernGeneratorCli.StructuredFeatureId.Timeouts] = this.buildTimeoutSnippets(); snippets[ReadmeSnippetBuilder.EXCEPTION_HANDLING_FEATURE_ID] = this.buildExceptionHandlingSnippets(); - snippets[ReadmeSnippetBuilder.RAW_RESPONSE_FEATURE_ID] = this.buildRawResponseSnippets(); + // gRPC APIs don't support raw response access or additional query parameters + if (!this.context.hasGrpcEndpoints()) { + snippets[ReadmeSnippetBuilder.RAW_RESPONSE_FEATURE_ID] = this.buildRawResponseSnippets(); + snippets[ReadmeSnippetBuilder.ADDITIONAL_QUERY_PARAMETERS_FEATURE_ID] = + this.buildAdditionalQueryParametersSnippets(); + } snippets[ReadmeSnippetBuilder.ADDITIONAL_HEADERS_FEATURE_ID] = this.buildAdditionalHeadersSnippets(); - snippets[ReadmeSnippetBuilder.ADDITIONAL_QUERY_PARAMETERS_FEATURE_ID] = - this.buildAdditionalQueryParametersSnippets(); if (this.isPaginationEnabled) { snippets[FernGeneratorCli.StructuredFeatureId.Pagination] = this.buildPaginationSnippets(); } diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index 83f9c7b56e6a..ef7ba6159c85 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -477,17 +477,17 @@ export class RootClientGenerator extends FileGenerator = {}; const ports: Record = {}; if (inspect) { diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/ExecutionEnvironment.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/ExecutionEnvironment.ts index e7e25d5fdad4..26fd93408913 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/ExecutionEnvironment.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/ExecutionEnvironment.ts @@ -2,6 +2,11 @@ import { ContainerRunner } from "@fern-api/core-utils"; import { AbsoluteFilePath } from "@fern-api/fs-utils"; import { TaskContext } from "@fern-api/task-context"; +export interface SourceMount { + hostPath: AbsoluteFilePath; + containerPath: string; +} + export declare namespace ExecutionEnvironment { interface ExecuteArgs { generatorName: string; @@ -11,6 +16,7 @@ export declare namespace ExecutionEnvironment { snippetPath?: AbsoluteFilePath; snippetTemplatePath?: AbsoluteFilePath; licenseFilePath?: AbsoluteFilePath; + sourceMounts?: SourceMount[]; context: TaskContext; inspect: boolean; runner: ContainerRunner | undefined; diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/ReusableContainerExecutionEnvironment.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/ReusableContainerExecutionEnvironment.ts index 2f1e405bb882..39b6dfecb3f1 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/ReusableContainerExecutionEnvironment.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/ReusableContainerExecutionEnvironment.ts @@ -14,7 +14,8 @@ import { CONTAINER_GENERATOR_CONFIG_PATH, CONTAINER_PATH_TO_IR, CONTAINER_PATH_TO_SNIPPET, - CONTAINER_PATH_TO_SNIPPET_TEMPLATES + CONTAINER_PATH_TO_SNIPPET_TEMPLATES, + CONTAINER_SOURCES_DIRECTORY } from "./constants.js"; import { ExecutionEnvironment } from "./ExecutionEnvironment.js"; @@ -146,6 +147,7 @@ export class ReusableContainerExecutionEnvironment implements ExecutionEnvironme snippetPath, snippetTemplatePath, licenseFilePath, + sourceMounts, context, inspect }: ExecutionEnvironment.ExecuteArgs @@ -172,7 +174,7 @@ export class ReusableContainerExecutionEnvironment implements ExecutionEnvironme command: [ "sh", "-c", - `rm -rf ${CONTAINER_FERN_DIRECTORY} /tmp/LICENSE && mkdir -p ${CONTAINER_CODEGEN_OUTPUT_DIRECTORY}` + `rm -rf ${CONTAINER_FERN_DIRECTORY} /tmp/LICENSE && mkdir -p ${CONTAINER_CODEGEN_OUTPUT_DIRECTORY} ${CONTAINER_SOURCES_DIRECTORY}` ], runner: this.runner, writeLogsToFile: false @@ -225,6 +227,16 @@ export class ReusableContainerExecutionEnvironment implements ExecutionEnvironme }); } + for (const sourceMount of sourceMounts ?? []) { + await copyToContainer({ + logger, + containerId, + hostPath: sourceMount.hostPath, + containerPath: sourceMount.containerPath, + runner: this.runner + }); + } + // Execute the generator inside the container using the image's original entrypoint await execInContainer({ logger, diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts index 7bfcd6835d1e..d62a5537ef2d 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts @@ -207,6 +207,14 @@ export async function writeFilesToDiskAndRunGenerator({ // Extract LICENSE file path for Docker mounting const absolutePathToLicenseFile = extractLicenseFilePath(generatorInvocation, absolutePathToFernConfig); + const sourceMounts = workspace + .getSources() + .filter((source): source is IdentifiableSource & { type: "protobuf" } => source.type === "protobuf") + .map((source) => ({ + hostPath: source.absoluteFilePath, + containerPath: `${CONTAINER_SOURCES_DIRECTORY}/${source.id}` + })); + await environment.execute({ generatorName: generatorInvocation.name, irPath: absolutePathToIr, @@ -215,6 +223,7 @@ export async function writeFilesToDiskAndRunGenerator({ snippetPath: absolutePathToTmpSnippetJSON, snippetTemplatePath: absolutePathToTmpSnippetTemplatesJSON, licenseFilePath: absolutePathToLicenseFile, + sourceMounts, context, inspect, runner diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/data/v1/data.proto b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/data/v1/data.proto new file mode 100644 index 000000000000..b8244b95516d --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/data/v1/data.proto @@ -0,0 +1,230 @@ +syntax = "proto3"; + +package data.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; + +option csharp_namespace = "Data.V1.Grpc"; +option go_package = "github.com/acme.co/data-go-grpc"; + +enum IndexType { + INDEX_TYPE_INVALID = 0; + INDEX_TYPE_DEFAULT = 1; + INDEX_TYPE_STRICT = 2; +} + +message IndexedData { + repeated uint32 indices = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; +} + +message Column { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct metadata = 3; + IndexedData indexed_data = 4; +} + +message ScoredColumn { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + float score = 2; + repeated float values = 3; + google.protobuf.Struct metadata = 4; + IndexedData indexed_data = 5; +} + +message UploadRequest { + repeated Column columns = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message UploadResponse { + uint32 count = 1; +} + +message DeleteRequest { + repeated string ids = 1; + bool delete_all = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; +} + +message DeleteResponse {} + +message FetchRequest { + repeated string ids = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message FetchResponse { + map columns = 1; + string namespace = 2; + optional Usage usage = 3; +} + +message ListRequest { + optional string prefix = 1; + optional uint32 limit = 2; + optional string pagination_token = 3; + string namespace = 4; +} + +message Pagination { + string next = 1; +} + +message ListElement { + string id = 1; +} + +message ListResponse { + repeated ListElement columns = 1; + optional Pagination pagination = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message QueryColumn { + repeated float values = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + uint32 top_k = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; + IndexedData indexed_data = 5; +} + +message QueryRequest { + string namespace = 1; + uint32 top_k = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct filter = 3; + bool include_values = 4; + bool include_metadata = 5; + repeated QueryColumn queries = 6 [ + deprecated = true + ]; + repeated float column = 7; + string id = 8; + IndexedData indexed_data = 9; +} + +message QueryResult { + repeated ScoredColumn matches = 1; + string namespace = 2; +} + +message QueryResponse { + repeated QueryResult results = 1 [deprecated=true]; + repeated ScoredColumn matches = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message Usage { + optional uint32 units = 1; +} + +message UpdateRequest { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2; + google.protobuf.Struct set_metadata = 3; + string namespace = 4; + IndexedData indexed_data = 5; + IndexType index_type = 6; + google.protobuf.Any details = 7; + repeated IndexType index_types = 8; +} + +message UpdateResponse { + google.protobuf.Timestamp updated_at = 1; + IndexType index_type = 2; + google.protobuf.Any details = 3; + repeated IndexType index_types = 4; +} + +message DescribeRequest { + google.protobuf.Struct filter = 1; + google.protobuf.Timestamp after = 2; +} + +message NamespaceSummary { + uint32 count = 1; +} + +message DescribeResponse { + map namespaces = 1; + uint32 dimension = 2; + float fullness = 3; + uint32 total_count = 4; +} + +service DataService { + rpc Upload(UploadRequest) returns (UploadResponse) { + option (google.api.http) = { + post: "/data" + body: "*" + }; + } + + rpc Delete(DeleteRequest) returns (DeleteResponse) { + option (google.api.http) = { + post: "/data/delete" + body: "*" + }; + } + + rpc Fetch(FetchRequest) returns (FetchResponse) { + option (google.api.http) = { + get: "/data/fetch" + }; + } + + rpc List(ListRequest) returns (ListResponse) { + option (google.api.http) = { + get: "/data/list" + }; + } + + rpc Query(QueryRequest) returns (QueryResponse) { + option (google.api.http) = { + post: "/data/query" + body: "*" + }; + } + + rpc Update(UpdateRequest) returns (UpdateResponse) { + option (google.api.http) = { + post: "/data/update" + body: "*" + }; + } + + rpc Describe(DescribeRequest) returns (DescribeResponse) { + option (google.api.http) = { + post: "/data/describe" + body: "*" + }; + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/annotations.proto b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/annotations.proto new file mode 100644 index 000000000000..8ff42098404c --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/field_behavior.proto b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/field_behavior.proto new file mode 100644 index 000000000000..128799c558db --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} \ No newline at end of file diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/http.proto b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/http.proto new file mode 100644 index 000000000000..c8392381eb99 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/proto/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Column.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Column.cs new file mode 100644 index 000000000000..ef225e7518ab --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Column.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Column : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Column type from its Protobuf-equivalent representation. + /// + internal static Column FromProto(ProtoDataV1Grpc.Column value) + { + return new Column + { + Id = value.Id, + Values = value.Values?.ToList() ?? Enumerable.Empty(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Column type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Column ToProto() + { + var result = new ProtoDataV1Grpc.Column(); + result.Id = Id; + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/ProtoAnyMapper.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/ProtoAnyMapper.cs new file mode 100644 index 000000000000..5c55aa625072 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/ProtoAnyMapper.cs @@ -0,0 +1,28 @@ +using global::System.Reflection; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi.Core; + +public static class ProtoAnyMapper +{ + public static Any? ToProto(object? value) + { + if (value is null) + { + return null; + } + var toProtoMethod = value + .GetType() + .GetMethod("ToProto", BindingFlags.Instance | BindingFlags.NonPublic); + if (toProtoMethod is null) + { + throw new InvalidOperationException( + $"Type {value.GetType()} does not have a ToProto method" + ); + } + var protoValue = toProtoMethod.Invoke(value, null); + return WellKnownProto.Any.Pack((IMessage)protoValue); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs new file mode 100644 index 000000000000..efdca475318f --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs @@ -0,0 +1,67 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct IndexType : IStringEnum +{ + public static readonly IndexType IndexTypeInvalid = new(Values.IndexTypeInvalid); + + public static readonly IndexType IndexTypeDefault = new(Values.IndexTypeDefault); + + public static readonly IndexType IndexTypeStrict = new(Values.IndexTypeStrict); + + public IndexType(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static IndexType FromCustom(string value) + { + return new IndexType(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(IndexType value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(IndexType value1, string value2) => !value1.Value.Equals(value2); + + public static explicit operator string(IndexType value) => value.Value; + + public static explicit operator IndexType(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string IndexTypeInvalid = "INDEX_TYPE_INVALID"; + + public const string IndexTypeDefault = "INDEX_TYPE_DEFAULT"; + + public const string IndexTypeStrict = "INDEX_TYPE_STRICT"; + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DeleteResponse.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DeleteResponse.cs new file mode 100644 index 000000000000..2c72cc2beea7 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DeleteResponse.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DeleteResponse type from its Protobuf-equivalent representation. + /// + internal static DeleteResponse FromProto(ProtoDataV1Grpc.DeleteResponse value) + { + return new DeleteResponse(); + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DeleteResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DeleteResponse ToProto() + { + return new ProtoDataV1Grpc.DeleteResponse(); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DescribeResponse.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DescribeResponse.cs new file mode 100644 index 000000000000..be7b2a6f4b01 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/DescribeResponse.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DescribeResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("namespaces")] + public Dictionary? Namespaces { get; set; } + + [JsonPropertyName("dimension")] + public uint? Dimension { get; set; } + + [JsonPropertyName("fullness")] + public float? Fullness { get; set; } + + [JsonPropertyName("total_count")] + public uint? TotalCount { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DescribeResponse type from its Protobuf-equivalent representation. + /// + internal static DescribeResponse FromProto(ProtoDataV1Grpc.DescribeResponse value) + { + return new DescribeResponse + { + Namespaces = value.Namespaces?.ToDictionary( + kvp => kvp.Key, + kvp => NamespaceSummary.FromProto(kvp.Value) + ), + Dimension = value.Dimension, + Fullness = value.Fullness, + TotalCount = value.TotalCount, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DescribeResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DescribeResponse ToProto() + { + var result = new ProtoDataV1Grpc.DescribeResponse(); + if (Namespaces != null && Namespaces.Any()) + { + foreach (var kvp in Namespaces) + { + result.Namespaces.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Dimension != null) + { + result.Dimension = Dimension ?? 0; + } + if (Fullness != null) + { + result.Fullness = Fullness ?? 0.0f; + } + if (TotalCount != null) + { + result.TotalCount = TotalCount ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FetchResponse.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FetchResponse.cs new file mode 100644 index 000000000000..0558b91da66e --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FetchResponse.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public Dictionary? Columns { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new FetchResponse type from its Protobuf-equivalent representation. + /// + internal static FetchResponse FromProto(ProtoDataV1Grpc.FetchResponse value) + { + return new FetchResponse + { + Columns = value.Columns?.ToDictionary( + kvp => kvp.Key, + kvp => Column.FromProto(kvp.Value) + ), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the FetchResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.FetchResponse ToProto() + { + var result = new ProtoDataV1Grpc.FetchResponse(); + if (Columns != null && Columns.Any()) + { + foreach (var kvp in Columns) + { + result.Columns.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs new file mode 100644 index 000000000000..38cc250adbcf --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs @@ -0,0 +1,95 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct FieldBehavior : IStringEnum +{ + public static readonly FieldBehavior FieldBehaviorUnspecified = new( + Values.FieldBehaviorUnspecified + ); + + public static readonly FieldBehavior Optional = new(Values.Optional); + + public static readonly FieldBehavior Required = new(Values.Required); + + public static readonly FieldBehavior OutputOnly = new(Values.OutputOnly); + + public static readonly FieldBehavior InputOnly = new(Values.InputOnly); + + public static readonly FieldBehavior Immutable = new(Values.Immutable); + + public static readonly FieldBehavior UnorderedList = new(Values.UnorderedList); + + public static readonly FieldBehavior NonEmptyDefault = new(Values.NonEmptyDefault); + + public static readonly FieldBehavior Identifier = new(Values.Identifier); + + public FieldBehavior(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static FieldBehavior FromCustom(string value) + { + return new FieldBehavior(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(FieldBehavior value1, string value2) => + value1.Value.Equals(value2); + + public static bool operator !=(FieldBehavior value1, string value2) => + !value1.Value.Equals(value2); + + public static explicit operator string(FieldBehavior value) => value.Value; + + public static explicit operator FieldBehavior(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string FieldBehaviorUnspecified = "FIELD_BEHAVIOR_UNSPECIFIED"; + + public const string Optional = "OPTIONAL"; + + public const string Required = "REQUIRED"; + + public const string OutputOnly = "OUTPUT_ONLY"; + + public const string InputOnly = "INPUT_ONLY"; + + public const string Immutable = "IMMUTABLE"; + + public const string UnorderedList = "UNORDERED_LIST"; + + public const string NonEmptyDefault = "NON_EMPTY_DEFAULT"; + + public const string Identifier = "IDENTIFIER"; + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/IndexedData.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/IndexedData.cs new file mode 100644 index 000000000000..4cf44e0ced78 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/IndexedData.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record IndexedData : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("indices")] + public IEnumerable Indices { get; set; } = new List(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new IndexedData type from its Protobuf-equivalent representation. + /// + internal static IndexedData FromProto(ProtoDataV1Grpc.IndexedData value) + { + return new IndexedData + { + Indices = value.Indices?.ToList() ?? Enumerable.Empty(), + Values = value.Values?.ToList() ?? Enumerable.Empty(), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the IndexedData type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.IndexedData ToProto() + { + var result = new ProtoDataV1Grpc.IndexedData(); + if (Indices.Any()) + { + result.Indices.AddRange(Indices); + } + if (Values.Any()) + { + result.Values.AddRange(Values); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListElement.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListElement.cs new file mode 100644 index 000000000000..ff0e19b9e9a9 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListElement.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListElement : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListElement type from its Protobuf-equivalent representation. + /// + internal static ListElement FromProto(ProtoDataV1Grpc.ListElement value) + { + return new ListElement { Id = value.Id }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListElement type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListElement ToProto() + { + var result = new ProtoDataV1Grpc.ListElement(); + if (Id != null) + { + result.Id = Id ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListResponse.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListResponse.cs new file mode 100644 index 000000000000..72b63f8287fd --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ListResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public IEnumerable? Columns { get; set; } + + [JsonPropertyName("pagination")] + public Pagination? Pagination { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListResponse type from its Protobuf-equivalent representation. + /// + internal static ListResponse FromProto(ProtoDataV1Grpc.ListResponse value) + { + return new ListResponse + { + Columns = value.Columns?.Select(ListElement.FromProto), + Pagination = value.Pagination != null ? Pagination.FromProto(value.Pagination) : null, + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListResponse ToProto() + { + var result = new ProtoDataV1Grpc.ListResponse(); + if (Columns != null && Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Pagination != null) + { + result.Pagination = Pagination.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Metadata.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Metadata.cs new file mode 100644 index 000000000000..a767a7f0bdd6 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Metadata.cs @@ -0,0 +1,39 @@ +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } + + internal static Metadata FromProto(WellKnownProto.Struct value) + { + var result = new Metadata(); + foreach (var kvp in value.Fields) + { + result[kvp.Key] = kvp.Value != null ? MetadataValue.FromProto(kvp.Value) : null; + } + return result; + } + + internal WellKnownProto.Struct ToProto() + { + var result = new WellKnownProto.Struct(); + foreach (var kvp in this) + { + result.Fields[kvp.Key] = kvp.Value?.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/MetadataValue.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/MetadataValue.cs new file mode 100644 index 000000000000..51b7b46595b3 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/MetadataValue.cs @@ -0,0 +1,91 @@ +using OneOf; +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class MetadataValue( + OneOf, Metadata> value +) : OneOfBase, Metadata>(value) +{ + internal static MetadataValue? FromProto(WellKnownProto.Value value) + { + return value.KindCase switch + { + WellKnownProto.Value.KindOneofCase.StringValue => value.StringValue, + WellKnownProto.Value.KindOneofCase.NumberValue => value.NumberValue, + WellKnownProto.Value.KindOneofCase.BoolValue => value.BoolValue, + WellKnownProto.Value.KindOneofCase.ListValue => value + .ListValue.Values.Select(FromProto) + .ToList(), + WellKnownProto.Value.KindOneofCase.StructValue => Metadata.FromProto(value.StructValue), + _ => null, + }; + } + + internal WellKnownProto.Value ToProto() + { + return Match( + WellKnownProto.Value.ForString, + WellKnownProto.Value.ForNumber, + WellKnownProto.Value.ForBool, + list => new WellKnownProto.Value + { + ListValue = new WellKnownProto.ListValue + { + Values = { list.Select(item => item?.ToProto()) }, + }, + }, + nested => new WellKnownProto.Value { StructValue = nested.ToProto() } + ); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } + + public static implicit operator MetadataValue(string value) => new(value); + + public static implicit operator MetadataValue(bool value) => new(value); + + public static implicit operator MetadataValue(double value) => new(value); + + public static implicit operator MetadataValue(Metadata value) => new(value); + + public static implicit operator MetadataValue(MetadataValue?[] value) => new(value); + + public static implicit operator MetadataValue(List value) => new(value); + + public static implicit operator MetadataValue(string[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(bool[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(bool?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/NamespaceSummary.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/NamespaceSummary.cs new file mode 100644 index 000000000000..b302c01763b5 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/NamespaceSummary.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record NamespaceSummary : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new NamespaceSummary type from its Protobuf-equivalent representation. + /// + internal static NamespaceSummary FromProto(ProtoDataV1Grpc.NamespaceSummary value) + { + return new NamespaceSummary { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the NamespaceSummary type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.NamespaceSummary ToProto() + { + var result = new ProtoDataV1Grpc.NamespaceSummary(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Pagination.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Pagination.cs new file mode 100644 index 000000000000..2eb1d1eeedec --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Pagination.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Pagination : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("next")] + public string? Next { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Pagination type from its Protobuf-equivalent representation. + /// + internal static Pagination FromProto(ProtoDataV1Grpc.Pagination value) + { + return new Pagination { Next = value.Next }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Pagination type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Pagination ToProto() + { + var result = new ProtoDataV1Grpc.Pagination(); + if (Next != null) + { + result.Next = Next ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryColumn.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryColumn.cs new file mode 100644 index 000000000000..d2621a6d87c6 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryColumn.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("top_k")] + public uint? TopK { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryColumn type from its Protobuf-equivalent representation. + /// + internal static QueryColumn FromProto(ProtoDataV1Grpc.QueryColumn value) + { + return new QueryColumn + { + Values = value.Values?.ToList() ?? Enumerable.Empty(), + TopK = value.TopK, + Namespace = value.Namespace, + Filter = value.Filter != null ? Metadata.FromProto(value.Filter) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryColumn ToProto() + { + var result = new ProtoDataV1Grpc.QueryColumn(); + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (TopK != null) + { + result.TopK = TopK ?? 0; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResponse.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResponse.cs new file mode 100644 index 000000000000..9242ccd094a1 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("results")] + public IEnumerable? Results { get; set; } + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResponse type from its Protobuf-equivalent representation. + /// + internal static QueryResponse FromProto(ProtoDataV1Grpc.QueryResponse value) + { + return new QueryResponse + { + Results = value.Results?.Select(QueryResult.FromProto), + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResponse ToProto() + { + var result = new ProtoDataV1Grpc.QueryResponse(); + if (Results != null && Results.Any()) + { + result.Results.AddRange(Results.Select(elem => elem.ToProto())); + } + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResult.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResult.cs new file mode 100644 index 000000000000..8a0ab47d124a --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/QueryResult.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResult : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResult type from its Protobuf-equivalent representation. + /// + internal static QueryResult FromProto(ProtoDataV1Grpc.QueryResult value) + { + return new QueryResult + { + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResult type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResult ToProto() + { + var result = new ProtoDataV1Grpc.QueryResult(); + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ScoredColumn.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ScoredColumn.cs new file mode 100644 index 000000000000..a0d7fc819768 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/ScoredColumn.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ScoredColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("score")] + public float? Score { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ScoredColumn type from its Protobuf-equivalent representation. + /// + internal static ScoredColumn FromProto(ProtoDataV1Grpc.ScoredColumn value) + { + return new ScoredColumn + { + Id = value.Id, + Score = value.Score, + Values = value.Values?.ToList(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ScoredColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ScoredColumn ToProto() + { + var result = new ProtoDataV1Grpc.ScoredColumn(); + result.Id = Id; + if (Score != null) + { + result.Score = Score ?? 0.0f; + } + if (Values != null && Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/SeedApi.csproj b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/SeedApi.csproj index 4bac237048e3..70240c028c83 100644 --- a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/SeedApi.csproj +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/SeedApi.csproj @@ -44,6 +44,39 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UpdateResponse.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UpdateResponse.cs new file mode 100644 index 000000000000..643f8a79e161 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UpdateResponse.cs @@ -0,0 +1,116 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record UpdateResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("updated_at")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UpdateResponse type from its Protobuf-equivalent representation. + /// + internal static UpdateResponse FromProto(ProtoDataV1Grpc.UpdateResponse value) + { + return new UpdateResponse + { + UpdatedAt = value.UpdatedAt.ToDateTime(), + IndexType = value.IndexType switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexType}"), + }, + Details = value.Details != null ? value.Details : null, + IndexTypes = value.IndexTypes.Select(type => + type switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexTypes}"), + } + ), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UpdateResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UpdateResponse ToProto() + { + var result = new ProtoDataV1Grpc.UpdateResponse(); + if (UpdatedAt != null) + { + result.UpdatedAt = WellKnownProto.Timestamp.FromDateTime( + UpdatedAt.Value.ToUniversalTime() + ); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UploadResponse.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UploadResponse.cs new file mode 100644 index 000000000000..2409ef7d7bff --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/UploadResponse.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UploadResponse type from its Protobuf-equivalent representation. + /// + internal static UploadResponse FromProto(ProtoDataV1Grpc.UploadResponse value) + { + return new UploadResponse { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UploadResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UploadResponse ToProto() + { + var result = new ProtoDataV1Grpc.UploadResponse(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Usage.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Usage.cs new file mode 100644 index 000000000000..c6c1ccf37d4a --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Usage.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Usage : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("units")] + public uint? Units { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Usage type from its Protobuf-equivalent representation. + /// + internal static Usage FromProto(ProtoDataV1Grpc.Usage value) + { + return new Usage { Units = value.Units }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Usage type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Usage ToProto() + { + var result = new ProtoDataV1Grpc.Usage(); + if (Units != null) + { + result.Units = Units ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto/proto/google/api/annotations.proto b/seed/csharp-model/csharp-grpc-proto/proto/google/api/annotations.proto new file mode 100644 index 000000000000..8ff42098404c --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/seed/csharp-model/csharp-grpc-proto/proto/google/api/http.proto b/seed/csharp-model/csharp-grpc-proto/proto/google/api/http.proto new file mode 100644 index 000000000000..c8392381eb99 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/proto/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/seed/csharp-model/csharp-grpc-proto/proto/user/v1/user.proto b/seed/csharp-model/csharp-grpc-proto/proto/user/v1/user.proto new file mode 100644 index 000000000000..28542ac965a1 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/proto/user/v1/user.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package user.v1; + +import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; + +option csharp_namespace = "User.V1"; +option go_package = "user/v1"; + +message UserModel { + string username = 1; + string email = 2; + uint32 age = 3; + float weight = 4; + google.protobuf.Struct metadata = 5; +} + +message CreateRequest { + string username = 1; + string email = 2; + uint32 age = 3; + float weight = 4; + google.protobuf.Struct metadata = 5; +} + +message CreateResponse { + UserModel user = 1; +} + +service UserService { + rpc Create(CreateRequest) returns (CreateResponse) { + option (google.api.http) = { + post: "/users" + body: "*" + }; + } +} \ No newline at end of file diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/CreateResponse.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/CreateResponse.cs new file mode 100644 index 000000000000..9e0d9e26441c --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/CreateResponse.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoUserV1 = User.V1; + +namespace SeedApi; + +[Serializable] +public record CreateResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("user")] + public UserModel? User { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new CreateResponse type from its Protobuf-equivalent representation. + /// + internal static CreateResponse FromProto(ProtoUserV1.CreateResponse value) + { + return new CreateResponse + { + User = value.User != null ? UserModel.FromProto(value.User) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the CreateResponse type into its Protobuf-equivalent representation. + /// + internal ProtoUserV1.CreateResponse ToProto() + { + var result = new ProtoUserV1.CreateResponse(); + if (User != null) + { + result.User = User.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Metadata.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Metadata.cs new file mode 100644 index 000000000000..a767a7f0bdd6 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Metadata.cs @@ -0,0 +1,39 @@ +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } + + internal static Metadata FromProto(WellKnownProto.Struct value) + { + var result = new Metadata(); + foreach (var kvp in value.Fields) + { + result[kvp.Key] = kvp.Value != null ? MetadataValue.FromProto(kvp.Value) : null; + } + return result; + } + + internal WellKnownProto.Struct ToProto() + { + var result = new WellKnownProto.Struct(); + foreach (var kvp in this) + { + result.Fields[kvp.Key] = kvp.Value?.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/MetadataValue.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/MetadataValue.cs new file mode 100644 index 000000000000..51b7b46595b3 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/MetadataValue.cs @@ -0,0 +1,91 @@ +using OneOf; +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class MetadataValue( + OneOf, Metadata> value +) : OneOfBase, Metadata>(value) +{ + internal static MetadataValue? FromProto(WellKnownProto.Value value) + { + return value.KindCase switch + { + WellKnownProto.Value.KindOneofCase.StringValue => value.StringValue, + WellKnownProto.Value.KindOneofCase.NumberValue => value.NumberValue, + WellKnownProto.Value.KindOneofCase.BoolValue => value.BoolValue, + WellKnownProto.Value.KindOneofCase.ListValue => value + .ListValue.Values.Select(FromProto) + .ToList(), + WellKnownProto.Value.KindOneofCase.StructValue => Metadata.FromProto(value.StructValue), + _ => null, + }; + } + + internal WellKnownProto.Value ToProto() + { + return Match( + WellKnownProto.Value.ForString, + WellKnownProto.Value.ForNumber, + WellKnownProto.Value.ForBool, + list => new WellKnownProto.Value + { + ListValue = new WellKnownProto.ListValue + { + Values = { list.Select(item => item?.ToProto()) }, + }, + }, + nested => new WellKnownProto.Value { StructValue = nested.ToProto() } + ); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } + + public static implicit operator MetadataValue(string value) => new(value); + + public static implicit operator MetadataValue(bool value) => new(value); + + public static implicit operator MetadataValue(double value) => new(value); + + public static implicit operator MetadataValue(Metadata value) => new(value); + + public static implicit operator MetadataValue(MetadataValue?[] value) => new(value); + + public static implicit operator MetadataValue(List value) => new(value); + + public static implicit operator MetadataValue(string[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(bool[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(bool?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); +} diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/SeedApi.csproj b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/SeedApi.csproj index d4d3cf0ae0db..7f0abbb33f49 100644 --- a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/SeedApi.csproj +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/SeedApi.csproj @@ -44,6 +44,34 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/UserModel.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/UserModel.cs new file mode 100644 index 000000000000..22c7cfb30155 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/UserModel.cs @@ -0,0 +1,85 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoUserV1 = User.V1; + +namespace SeedApi; + +[Serializable] +public record UserModel : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("username")] + public string? Username { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("age")] + public uint? Age { get; set; } + + [JsonPropertyName("weight")] + public float? Weight { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UserModel type from its Protobuf-equivalent representation. + /// + internal static UserModel FromProto(ProtoUserV1.UserModel value) + { + return new UserModel + { + Username = value.Username, + Email = value.Email, + Age = value.Age, + Weight = value.Weight, + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UserModel type into its Protobuf-equivalent representation. + /// + internal ProtoUserV1.UserModel ToProto() + { + var result = new ProtoUserV1.UserModel(); + if (Username != null) + { + result.Username = Username ?? ""; + } + if (Email != null) + { + result.Email = Email ?? ""; + } + if (Age != null) + { + result.Age = Age ?? 0; + } + if (Weight != null) + { + result.Weight = Weight ?? 0.0f; + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/README.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/README.md index 79564745a6ec..7cc184191f18 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/README.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/README.md @@ -15,9 +15,8 @@ The Seed C# library provides convenient access to the Seed APIs from C#. - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) - - [Raw Response](#raw-response) - [Additional Headers](#additional-headers) - - [Additional Query Parameters](#additional-query-parameters) + - [Forward Compatible Enums](#forward-compatible-enums) - [Contributing](#contributing) ## Requirements @@ -99,34 +98,6 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Raw Response - -Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. - -```csharp -using SeedApi; - -// Access raw response data (status code, headers, etc.) alongside the parsed response -var result = await client.Dataservice.FooAsync(...).WithRawResponse(); - -// Access the parsed data -var data = result.Data; - -// Access raw response metadata -var statusCode = result.RawResponse.StatusCode; -var headers = result.RawResponse.Headers; -var url = result.RawResponse.Url; - -// Access specific headers (case-insensitive) -if (headers.TryGetValue("X-Request-Id", out var requestId)) -{ - System.Console.WriteLine($"Request ID: {requestId}"); -} - -// For the default behavior, simply await without .WithRawResponse() -var data = await client.Dataservice.FooAsync(...); -``` - ### Additional Headers If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. @@ -143,20 +114,33 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Additional Query Parameters +### Forward Compatible Enums -If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. +This SDK uses forward-compatible enums that can handle unknown values gracefully. ```csharp -var response = await client.Dataservice.FooAsync( - ..., - new RequestOptions { - AdditionalQueryParameters = new Dictionary - { - { "custom_param", "custom-value" } - } - } -); +using SeedApi; + +// Using a built-in value +var indexType = IndexType.IndexTypeInvalid; + +// Using a custom value +var customIndexType = IndexType.FromCustom("custom-value"); + +// Using in a switch statement +switch (indexType.Value) +{ + case IndexType.Values.IndexTypeInvalid: + Console.WriteLine("IndexTypeInvalid"); + break; + default: + Console.WriteLine($"Unknown value: {indexType.Value}"); + break; +} + +// Explicit casting +string indexTypeString = (string)IndexType.IndexTypeInvalid; +IndexType indexTypeFromString = (IndexType)"INDEX_TYPE_INVALID"; ``` ## Contributing diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/data/v1/data.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/data/v1/data.proto new file mode 100644 index 000000000000..b8244b95516d --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/data/v1/data.proto @@ -0,0 +1,230 @@ +syntax = "proto3"; + +package data.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; + +option csharp_namespace = "Data.V1.Grpc"; +option go_package = "github.com/acme.co/data-go-grpc"; + +enum IndexType { + INDEX_TYPE_INVALID = 0; + INDEX_TYPE_DEFAULT = 1; + INDEX_TYPE_STRICT = 2; +} + +message IndexedData { + repeated uint32 indices = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; +} + +message Column { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct metadata = 3; + IndexedData indexed_data = 4; +} + +message ScoredColumn { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + float score = 2; + repeated float values = 3; + google.protobuf.Struct metadata = 4; + IndexedData indexed_data = 5; +} + +message UploadRequest { + repeated Column columns = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message UploadResponse { + uint32 count = 1; +} + +message DeleteRequest { + repeated string ids = 1; + bool delete_all = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; +} + +message DeleteResponse {} + +message FetchRequest { + repeated string ids = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message FetchResponse { + map columns = 1; + string namespace = 2; + optional Usage usage = 3; +} + +message ListRequest { + optional string prefix = 1; + optional uint32 limit = 2; + optional string pagination_token = 3; + string namespace = 4; +} + +message Pagination { + string next = 1; +} + +message ListElement { + string id = 1; +} + +message ListResponse { + repeated ListElement columns = 1; + optional Pagination pagination = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message QueryColumn { + repeated float values = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + uint32 top_k = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; + IndexedData indexed_data = 5; +} + +message QueryRequest { + string namespace = 1; + uint32 top_k = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct filter = 3; + bool include_values = 4; + bool include_metadata = 5; + repeated QueryColumn queries = 6 [ + deprecated = true + ]; + repeated float column = 7; + string id = 8; + IndexedData indexed_data = 9; +} + +message QueryResult { + repeated ScoredColumn matches = 1; + string namespace = 2; +} + +message QueryResponse { + repeated QueryResult results = 1 [deprecated=true]; + repeated ScoredColumn matches = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message Usage { + optional uint32 units = 1; +} + +message UpdateRequest { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2; + google.protobuf.Struct set_metadata = 3; + string namespace = 4; + IndexedData indexed_data = 5; + IndexType index_type = 6; + google.protobuf.Any details = 7; + repeated IndexType index_types = 8; +} + +message UpdateResponse { + google.protobuf.Timestamp updated_at = 1; + IndexType index_type = 2; + google.protobuf.Any details = 3; + repeated IndexType index_types = 4; +} + +message DescribeRequest { + google.protobuf.Struct filter = 1; + google.protobuf.Timestamp after = 2; +} + +message NamespaceSummary { + uint32 count = 1; +} + +message DescribeResponse { + map namespaces = 1; + uint32 dimension = 2; + float fullness = 3; + uint32 total_count = 4; +} + +service DataService { + rpc Upload(UploadRequest) returns (UploadResponse) { + option (google.api.http) = { + post: "/data" + body: "*" + }; + } + + rpc Delete(DeleteRequest) returns (DeleteResponse) { + option (google.api.http) = { + post: "/data/delete" + body: "*" + }; + } + + rpc Fetch(FetchRequest) returns (FetchResponse) { + option (google.api.http) = { + get: "/data/fetch" + }; + } + + rpc List(ListRequest) returns (ListResponse) { + option (google.api.http) = { + get: "/data/list" + }; + } + + rpc Query(QueryRequest) returns (QueryResponse) { + option (google.api.http) = { + post: "/data/query" + body: "*" + }; + } + + rpc Update(UpdateRequest) returns (UpdateResponse) { + option (google.api.http) = { + post: "/data/update" + body: "*" + }; + } + + rpc Describe(DescribeRequest) returns (DescribeResponse) { + option (google.api.http) = { + post: "/data/describe" + body: "*" + }; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/annotations.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/annotations.proto new file mode 100644 index 000000000000..8ff42098404c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/field_behavior.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/field_behavior.proto new file mode 100644 index 000000000000..128799c558db --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/http.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/http.proto new file mode 100644 index 000000000000..c8392381eb99 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/proto/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/reference.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/reference.md index bfc647a5cfb6..b5f9228a5c91 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/reference.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/reference.md @@ -1,5 +1,5 @@ # Reference -## Dataservice +## DataService
client.Dataservice.FooAsync() -> WithRawResponseTask<Dictionary<string, object?>>
@@ -25,3 +25,295 @@ await client.Dataservice.FooAsync();
+
client.Dataservice.UploadAsync(UploadRequest { ... }) -> WithRawResponseTask<UploadResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UploadAsync( + new UploadRequest + { + Columns = new List() + { + new SeedApi.Column + { + Id = "id", + Values = new List() { 1.1f }, + }, + }, + } +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UploadRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DeleteAsync(DeleteRequest { ... }) -> WithRawResponseTask<DeleteResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DeleteAsync(new DeleteRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DeleteRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DescribeAsync(DescribeRequest { ... }) -> WithRawResponseTask<DescribeResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DescribeAsync(new DescribeRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DescribeRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.FetchAsync(FetchRequest { ... }) -> WithRawResponseTask<FetchResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.FetchAsync(new FetchRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `FetchRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.ListAsync(ListRequest { ... }) -> WithRawResponseTask<ListResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.ListAsync(new ListRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ListRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.QueryAsync(QueryRequest { ... }) -> WithRawResponseTask<QueryResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `QueryRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.UpdateAsync(UpdateRequest { ... }) -> WithRawResponseTask<UpdateResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UpdateRequest` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/snippet.json b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/snippet.json index 6cef6bd0ec54..10d4d0868455 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/snippet.json +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/snippet.json @@ -12,6 +12,90 @@ "type": "csharp", "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FooAsync();\n" } + }, + { + "example_identifier": null, + "id": { + "path": "/data", + "method": "POST", + "identifier_override": "endpoint_dataservice.upload" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UploadAsync(\n new UploadRequest\n {\n Columns = new List()\n {\n new SeedApi.Column\n {\n Id = \"id\",\n Values = new List() { 1.1f },\n },\n },\n }\n);\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/delete", + "method": "POST", + "identifier_override": "endpoint_dataservice.delete" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DeleteAsync(new DeleteRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/describe", + "method": "POST", + "identifier_override": "endpoint_dataservice.describe" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DescribeAsync(new DescribeRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/fetch", + "method": "GET", + "identifier_override": "endpoint_dataservice.fetch" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FetchAsync(new FetchRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/list", + "method": "GET", + "identifier_override": "endpoint_dataservice.list" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.ListAsync(new ListRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/query", + "method": "POST", + "identifier_override": "endpoint_dataservice.query" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/update", + "method": "POST", + "identifier_override": "endpoint_dataservice.update" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UpdateAsync(new UpdateRequest { Id = \"id\" });\n" + } } ] } \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example0.cs deleted file mode 100644 index 5c845cf2e096..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example0.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example0 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example1.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example1.cs deleted file mode 100644 index dc9e849f6c96..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/Example1.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example1 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj deleted file mode 100644 index 3417db2e58e2..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - 12 - enable - enable - - - - - - \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs deleted file mode 100644 index cb111a797b9a..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using NUnit.Framework; -using SeedApi; -using WireMock.Logging; -using WireMock.Server; -using WireMock.Settings; - -namespace SeedApi.Test.Unit.MockServer; - -[SetUpFixture] -public class BaseMockServerTest -{ - protected static WireMockServer Server { get; set; } = null!; - - protected static SeedApiClient Client { get; set; } = null!; - - protected static RequestOptions RequestOptions { get; set; } = new(); - - [OneTimeSetUp] - public void GlobalSetup() - { - // Start the WireMock server - Server = WireMockServer.Start( - new WireMockServerSettings { Logger = new WireMockConsoleLogger() } - ); - - // Initialize the Client - Client = new SeedApiClient( - clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } - ); - } - - [OneTimeTearDown] - public void GlobalTeardown() - { - Server.Stop(); - Server.Dispose(); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs deleted file mode 100644 index 3a6c904371a3..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using NUnit.Framework; -using SeedApi.Test.Unit.MockServer; -using SeedApi.Test.Utils; - -namespace SeedApi.Test.Unit.MockServer.Dataservice; - -[TestFixture] -public class FooTest : BaseMockServerTest -{ - [NUnit.Framework.Test] - public async Task MockServerTest_1() - { - const string mockResponse = """ - { - "string": { - "key": "value" - } - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } - - [NUnit.Framework.Test] - public async Task MockServerTest_2() - { - const string mockResponse = """ - { - "key": "value" - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/ProtoAnyMapper.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/ProtoAnyMapper.cs new file mode 100644 index 000000000000..5c55aa625072 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/ProtoAnyMapper.cs @@ -0,0 +1,28 @@ +using global::System.Reflection; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi.Core; + +public static class ProtoAnyMapper +{ + public static Any? ToProto(object? value) + { + if (value is null) + { + return null; + } + var toProtoMethod = value + .GetType() + .GetMethod("ToProto", BindingFlags.Instance | BindingFlags.NonPublic); + if (toProtoMethod is null) + { + throw new InvalidOperationException( + $"Type {value.GetType()} does not have a ToProto method" + ); + } + var protoValue = toProtoMethod.Invoke(value, null); + return WellKnownProto.Any.Pack((IMessage)protoValue); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs index 882ac111c6cd..e853d4e435f9 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/ClientOptions.cs @@ -1,3 +1,4 @@ +using Grpc.Net.Client; using SeedApi.Core; namespace SeedApi; @@ -71,6 +72,17 @@ public partial class ClientOptions #endif } = TimeSpan.FromSeconds(30); + /// + /// The options used for gRPC client endpoints. + /// + public GrpcChannelOptions? GrpcOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + /// /// Clones this and returns a new instance /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/GrpcRequestOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/GrpcRequestOptions.cs new file mode 100644 index 000000000000..4fb311172634 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/Public/GrpcRequestOptions.cs @@ -0,0 +1,63 @@ +using Grpc.Core; +using SeedApi.Core; + +namespace SeedApi; + +public partial class GrpcRequestOptions +{ + /// + /// The maximum number of retry attempts. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Options for write operations. + /// + public WriteOptions? WriteOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Client-side call credentials. Provide authorization with per-call granularity. + /// + public CallCredentials? CallCredentials { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with this particular request. + /// Headers with matching keys will be overwritten by headers set on the client options. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new List>(); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawClient.cs index d42791fcc0e0..3e191c04dbe4 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawClient.cs @@ -20,6 +20,13 @@ internal partial class RawClient(ClientOptions clientOptions) #endif internal int BaseRetryDelay { get; set; } = 1000; + private readonly Lazy _grpc = new(() => new RawGrpcClient(clientOptions)); + + /// + /// The gRPC client used to make requests. + /// + public RawGrpcClient Grpc => _grpc.Value; + /// /// The client options applied on every request. /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawGrpcClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawGrpcClient.cs new file mode 100644 index 000000000000..2326d6b36f8c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/RawGrpcClient.cs @@ -0,0 +1,61 @@ +using Grpc.Core; +using Grpc.Net.Client; + +namespace SeedApi.Core; + +/// +/// Utility class for making gRPC requests to the API. +/// +internal class RawGrpcClient +{ + /// + /// The gRPC channel used to make requests. + /// + public readonly GrpcChannel Channel; + + private readonly ClientOptions _clientOptions; + + public RawGrpcClient(ClientOptions clientOptions) + { + _clientOptions = clientOptions; + + var grpcOptions = PrepareGrpcChannelOptions(); + Channel = grpcOptions is not null + ? GrpcChannel.ForAddress(_clientOptions.BaseUrl, grpcOptions) + : GrpcChannel.ForAddress(_clientOptions.BaseUrl); + } + + /// + /// Creates CallOptions for a gRPC request with the provided metadata, timeout, and credentials. + /// Metadata (headers) should be built at the endpoint level before calling this method. + /// + public CallOptions CreateCallOptions( + global::Grpc.Core.Metadata metadata, + GrpcRequestOptions options, + CancellationToken cancellationToken = default + ) + { + var timeout = options.Timeout ?? _clientOptions.Timeout; + var deadline = DateTime.UtcNow.Add(timeout); + return new CallOptions( + metadata, + deadline, + cancellationToken, + options.WriteOptions, + null, + options.CallCredentials + ); + } + + private GrpcChannelOptions? PrepareGrpcChannelOptions() + { + var grpcChannelOptions = _clientOptions.GrpcOptions; + if (grpcChannelOptions is null) + { + return null; + } + grpcChannelOptions.HttpClient ??= _clientOptions.HttpClient; + grpcChannelOptions.MaxRetryAttempts ??= _clientOptions.MaxRetries; + return grpcChannelOptions; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs index 009bf4fe889b..0e7a5b1834a6 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/DataserviceClient.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using Data.V1.Grpc; +using Grpc.Core; using SeedApi.Core; namespace SeedApi; @@ -7,11 +9,17 @@ public partial class DataserviceClient : IDataserviceClient { private readonly RawClient _client; + private readonly RawGrpcClient _grpc; + + private DataService.DataServiceClient _dataService; + internal DataserviceClient(RawClient client) { try { _client = client; + _grpc = _client.Grpc; + _dataService = new DataService.DataServiceClient(_grpc.Channel); } catch (Exception ex) { @@ -105,4 +113,457 @@ internal DataserviceClient(RawClient client) FooAsyncCore(options, cancellationToken) ); } + + /// + /// await client.Dataservice.UploadAsync( + /// new UploadRequest + /// { + /// Columns = new List<SeedApi.Column>() + /// { + /// new SeedApi.Column + /// { + /// Id = "id", + /// Values = new List<float>() { 1.1f }, + /// }, + /// }, + /// } + /// ); + /// + public async Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UploadAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UploadResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + }) + .ConfigureAwait(false); + } + + /// + /// await client.Dataservice.DeleteAsync(new DeleteRequest()); + /// + public async Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DeleteAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DeleteResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + }) + .ConfigureAwait(false); + } + + /// + /// await client.Dataservice.DescribeAsync(new DescribeRequest()); + /// + public async Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DescribeAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DescribeResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + }) + .ConfigureAwait(false); + } + + /// + /// await client.Dataservice.FetchAsync(new FetchRequest()); + /// + public async Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.FetchAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return FetchResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + }) + .ConfigureAwait(false); + } + + /// + /// await client.Dataservice.ListAsync(new ListRequest()); + /// + public async Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.ListAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return ListResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + }) + .ConfigureAwait(false); + } + + /// + /// await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); + /// + public async Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.QueryAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return QueryResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + }) + .ConfigureAwait(false); + } + + /// + /// await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); + /// + public async Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return await _client + .Options.ExceptionHandler.TryCatchAsync(async () => + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UpdateAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UpdateResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + }) + .ConfigureAwait(false); + } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/IDataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/IDataserviceClient.cs index 790e52ff98cd..4a7de66ccb27 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/IDataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/IDataserviceClient.cs @@ -6,4 +6,46 @@ public partial interface IDataserviceClient RequestOptions? options = null, CancellationToken cancellationToken = default ); + + Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DeleteRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DeleteRequest.cs new file mode 100644 index 000000000000..6226129bb2dc --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DeleteRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteRequest +{ + [JsonPropertyName("ids")] + public IEnumerable? Ids { get; set; } + + [JsonPropertyName("delete_all")] + public bool? DeleteAll { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + /// + /// Maps the DeleteRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DeleteRequest ToProto() + { + var result = new Proto.DeleteRequest(); + if (Ids != null && Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (DeleteAll != null) + { + result.DeleteAll = DeleteAll ?? false; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DescribeRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DescribeRequest.cs new file mode 100644 index 000000000000..fcc72788bd2c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/DescribeRequest.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record DescribeRequest +{ + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("after")] + public DateTime? After { get; set; } + + /// + /// Maps the DescribeRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DescribeRequest ToProto() + { + var result = new Proto.DescribeRequest(); + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (After != null) + { + result.After = WellKnownProto.Timestamp.FromDateTime(After.Value.ToUniversalTime()); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/FetchRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/FetchRequest.cs new file mode 100644 index 000000000000..3ca54d9f8be1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/FetchRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchRequest +{ + [JsonIgnore] + public IEnumerable Ids { get; set; } = new List(); + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the FetchRequest type into its Protobuf-equivalent representation. + /// + internal Proto.FetchRequest ToProto() + { + var result = new Proto.FetchRequest(); + if (Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/ListRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/ListRequest.cs new file mode 100644 index 000000000000..fd26665984e4 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/ListRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListRequest +{ + [JsonIgnore] + public string? Prefix { get; set; } + + [JsonIgnore] + public uint? Limit { get; set; } + + [JsonIgnore] + public string? PaginationToken { get; set; } + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the ListRequest type into its Protobuf-equivalent representation. + /// + internal Proto.ListRequest ToProto() + { + var result = new Proto.ListRequest(); + if (Prefix != null) + { + result.Prefix = Prefix ?? ""; + } + if (Limit != null) + { + result.Limit = Limit ?? 0; + } + if (PaginationToken != null) + { + result.PaginationToken = PaginationToken ?? ""; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/QueryRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/QueryRequest.cs new file mode 100644 index 000000000000..0586a3c144b2 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/QueryRequest.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryRequest +{ + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("top_k")] + public required uint TopK { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("include_values")] + public bool? IncludeValues { get; set; } + + [JsonPropertyName("include_metadata")] + public bool? IncludeMetadata { get; set; } + + [JsonPropertyName("queries")] + public IEnumerable? Queries { get; set; } + + [JsonPropertyName("column")] + public IEnumerable? Column { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + /// + /// Maps the QueryRequest type into its Protobuf-equivalent representation. + /// + internal Proto.QueryRequest ToProto() + { + var result = new Proto.QueryRequest(); + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + result.TopK = TopK; + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IncludeValues != null) + { + result.IncludeValues = IncludeValues ?? false; + } + if (IncludeMetadata != null) + { + result.IncludeMetadata = IncludeMetadata ?? false; + } + if (Queries != null && Queries.Any()) + { + result.Queries.AddRange(Queries.Select(elem => elem.ToProto())); + } + if (Column != null && Column.Any()) + { + result.Column.AddRange(Column); + } + if (Id != null) + { + result.Id = Id ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UpdateRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UpdateRequest.cs new file mode 100644 index 000000000000..40238253ea5e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UpdateRequest.cs @@ -0,0 +1,100 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UpdateRequest +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonPropertyName("set_metadata")] + public Metadata? SetMetadata { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + /// + /// Maps the UpdateRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UpdateRequest ToProto() + { + var result = new Proto.UpdateRequest(); + result.Id = Id; + if (Values != null && Values.Any()) + { + result.Values.AddRange(Values); + } + if (SetMetadata != null) + { + result.SetMetadata = SetMetadata.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UploadRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UploadRequest.cs new file mode 100644 index 000000000000..b8b1d3749c34 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Requests/UploadRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadRequest +{ + [JsonPropertyName("columns")] + public IEnumerable Columns { get; set; } = new List(); + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + /// + /// Maps the UploadRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UploadRequest ToProto() + { + var result = new Proto.UploadRequest(); + if (Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs new file mode 100644 index 000000000000..efdca475318f --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs @@ -0,0 +1,67 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct IndexType : IStringEnum +{ + public static readonly IndexType IndexTypeInvalid = new(Values.IndexTypeInvalid); + + public static readonly IndexType IndexTypeDefault = new(Values.IndexTypeDefault); + + public static readonly IndexType IndexTypeStrict = new(Values.IndexTypeStrict); + + public IndexType(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static IndexType FromCustom(string value) + { + return new IndexType(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(IndexType value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(IndexType value1, string value2) => !value1.Value.Equals(value2); + + public static explicit operator string(IndexType value) => value.Value; + + public static explicit operator IndexType(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string IndexTypeInvalid = "INDEX_TYPE_INVALID"; + + public const string IndexTypeDefault = "INDEX_TYPE_DEFAULT"; + + public const string IndexTypeStrict = "INDEX_TYPE_STRICT"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApi.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApi.csproj index 4bac237048e3..70240c028c83 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApi.csproj +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/SeedApi.csproj @@ -44,6 +44,39 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Column.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Column.cs new file mode 100644 index 000000000000..ef225e7518ab --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Column.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Column : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Column type from its Protobuf-equivalent representation. + /// + internal static Column FromProto(ProtoDataV1Grpc.Column value) + { + return new Column + { + Id = value.Id, + Values = value.Values?.ToList() ?? Enumerable.Empty(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Column type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Column ToProto() + { + var result = new ProtoDataV1Grpc.Column(); + result.Id = Id; + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DeleteResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DeleteResponse.cs new file mode 100644 index 000000000000..2c72cc2beea7 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DeleteResponse.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DeleteResponse type from its Protobuf-equivalent representation. + /// + internal static DeleteResponse FromProto(ProtoDataV1Grpc.DeleteResponse value) + { + return new DeleteResponse(); + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DeleteResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DeleteResponse ToProto() + { + return new ProtoDataV1Grpc.DeleteResponse(); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DescribeResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DescribeResponse.cs new file mode 100644 index 000000000000..be7b2a6f4b01 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/DescribeResponse.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DescribeResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("namespaces")] + public Dictionary? Namespaces { get; set; } + + [JsonPropertyName("dimension")] + public uint? Dimension { get; set; } + + [JsonPropertyName("fullness")] + public float? Fullness { get; set; } + + [JsonPropertyName("total_count")] + public uint? TotalCount { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DescribeResponse type from its Protobuf-equivalent representation. + /// + internal static DescribeResponse FromProto(ProtoDataV1Grpc.DescribeResponse value) + { + return new DescribeResponse + { + Namespaces = value.Namespaces?.ToDictionary( + kvp => kvp.Key, + kvp => NamespaceSummary.FromProto(kvp.Value) + ), + Dimension = value.Dimension, + Fullness = value.Fullness, + TotalCount = value.TotalCount, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DescribeResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DescribeResponse ToProto() + { + var result = new ProtoDataV1Grpc.DescribeResponse(); + if (Namespaces != null && Namespaces.Any()) + { + foreach (var kvp in Namespaces) + { + result.Namespaces.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Dimension != null) + { + result.Dimension = Dimension ?? 0; + } + if (Fullness != null) + { + result.Fullness = Fullness ?? 0.0f; + } + if (TotalCount != null) + { + result.TotalCount = TotalCount ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FetchResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FetchResponse.cs new file mode 100644 index 000000000000..0558b91da66e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FetchResponse.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public Dictionary? Columns { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new FetchResponse type from its Protobuf-equivalent representation. + /// + internal static FetchResponse FromProto(ProtoDataV1Grpc.FetchResponse value) + { + return new FetchResponse + { + Columns = value.Columns?.ToDictionary( + kvp => kvp.Key, + kvp => Column.FromProto(kvp.Value) + ), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the FetchResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.FetchResponse ToProto() + { + var result = new ProtoDataV1Grpc.FetchResponse(); + if (Columns != null && Columns.Any()) + { + foreach (var kvp in Columns) + { + result.Columns.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs new file mode 100644 index 000000000000..38cc250adbcf --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs @@ -0,0 +1,95 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct FieldBehavior : IStringEnum +{ + public static readonly FieldBehavior FieldBehaviorUnspecified = new( + Values.FieldBehaviorUnspecified + ); + + public static readonly FieldBehavior Optional = new(Values.Optional); + + public static readonly FieldBehavior Required = new(Values.Required); + + public static readonly FieldBehavior OutputOnly = new(Values.OutputOnly); + + public static readonly FieldBehavior InputOnly = new(Values.InputOnly); + + public static readonly FieldBehavior Immutable = new(Values.Immutable); + + public static readonly FieldBehavior UnorderedList = new(Values.UnorderedList); + + public static readonly FieldBehavior NonEmptyDefault = new(Values.NonEmptyDefault); + + public static readonly FieldBehavior Identifier = new(Values.Identifier); + + public FieldBehavior(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static FieldBehavior FromCustom(string value) + { + return new FieldBehavior(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(FieldBehavior value1, string value2) => + value1.Value.Equals(value2); + + public static bool operator !=(FieldBehavior value1, string value2) => + !value1.Value.Equals(value2); + + public static explicit operator string(FieldBehavior value) => value.Value; + + public static explicit operator FieldBehavior(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string FieldBehaviorUnspecified = "FIELD_BEHAVIOR_UNSPECIFIED"; + + public const string Optional = "OPTIONAL"; + + public const string Required = "REQUIRED"; + + public const string OutputOnly = "OUTPUT_ONLY"; + + public const string InputOnly = "INPUT_ONLY"; + + public const string Immutable = "IMMUTABLE"; + + public const string UnorderedList = "UNORDERED_LIST"; + + public const string NonEmptyDefault = "NON_EMPTY_DEFAULT"; + + public const string Identifier = "IDENTIFIER"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/IndexedData.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/IndexedData.cs new file mode 100644 index 000000000000..4cf44e0ced78 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/IndexedData.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record IndexedData : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("indices")] + public IEnumerable Indices { get; set; } = new List(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new IndexedData type from its Protobuf-equivalent representation. + /// + internal static IndexedData FromProto(ProtoDataV1Grpc.IndexedData value) + { + return new IndexedData + { + Indices = value.Indices?.ToList() ?? Enumerable.Empty(), + Values = value.Values?.ToList() ?? Enumerable.Empty(), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the IndexedData type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.IndexedData ToProto() + { + var result = new ProtoDataV1Grpc.IndexedData(); + if (Indices.Any()) + { + result.Indices.AddRange(Indices); + } + if (Values.Any()) + { + result.Values.AddRange(Values); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListElement.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListElement.cs new file mode 100644 index 000000000000..ff0e19b9e9a9 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListElement.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListElement : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListElement type from its Protobuf-equivalent representation. + /// + internal static ListElement FromProto(ProtoDataV1Grpc.ListElement value) + { + return new ListElement { Id = value.Id }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListElement type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListElement ToProto() + { + var result = new ProtoDataV1Grpc.ListElement(); + if (Id != null) + { + result.Id = Id ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListResponse.cs new file mode 100644 index 000000000000..72b63f8287fd --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ListResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public IEnumerable? Columns { get; set; } + + [JsonPropertyName("pagination")] + public Pagination? Pagination { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListResponse type from its Protobuf-equivalent representation. + /// + internal static ListResponse FromProto(ProtoDataV1Grpc.ListResponse value) + { + return new ListResponse + { + Columns = value.Columns?.Select(ListElement.FromProto), + Pagination = value.Pagination != null ? Pagination.FromProto(value.Pagination) : null, + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListResponse ToProto() + { + var result = new ProtoDataV1Grpc.ListResponse(); + if (Columns != null && Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Pagination != null) + { + result.Pagination = Pagination.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Metadata.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Metadata.cs new file mode 100644 index 000000000000..a767a7f0bdd6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Metadata.cs @@ -0,0 +1,39 @@ +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } + + internal static Metadata FromProto(WellKnownProto.Struct value) + { + var result = new Metadata(); + foreach (var kvp in value.Fields) + { + result[kvp.Key] = kvp.Value != null ? MetadataValue.FromProto(kvp.Value) : null; + } + return result; + } + + internal WellKnownProto.Struct ToProto() + { + var result = new WellKnownProto.Struct(); + foreach (var kvp in this) + { + result.Fields[kvp.Key] = kvp.Value?.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/MetadataValue.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/MetadataValue.cs new file mode 100644 index 000000000000..51b7b46595b3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/MetadataValue.cs @@ -0,0 +1,91 @@ +using OneOf; +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class MetadataValue( + OneOf, Metadata> value +) : OneOfBase, Metadata>(value) +{ + internal static MetadataValue? FromProto(WellKnownProto.Value value) + { + return value.KindCase switch + { + WellKnownProto.Value.KindOneofCase.StringValue => value.StringValue, + WellKnownProto.Value.KindOneofCase.NumberValue => value.NumberValue, + WellKnownProto.Value.KindOneofCase.BoolValue => value.BoolValue, + WellKnownProto.Value.KindOneofCase.ListValue => value + .ListValue.Values.Select(FromProto) + .ToList(), + WellKnownProto.Value.KindOneofCase.StructValue => Metadata.FromProto(value.StructValue), + _ => null, + }; + } + + internal WellKnownProto.Value ToProto() + { + return Match( + WellKnownProto.Value.ForString, + WellKnownProto.Value.ForNumber, + WellKnownProto.Value.ForBool, + list => new WellKnownProto.Value + { + ListValue = new WellKnownProto.ListValue + { + Values = { list.Select(item => item?.ToProto()) }, + }, + }, + nested => new WellKnownProto.Value { StructValue = nested.ToProto() } + ); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } + + public static implicit operator MetadataValue(string value) => new(value); + + public static implicit operator MetadataValue(bool value) => new(value); + + public static implicit operator MetadataValue(double value) => new(value); + + public static implicit operator MetadataValue(Metadata value) => new(value); + + public static implicit operator MetadataValue(MetadataValue?[] value) => new(value); + + public static implicit operator MetadataValue(List value) => new(value); + + public static implicit operator MetadataValue(string[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(bool[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(bool?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/NamespaceSummary.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/NamespaceSummary.cs new file mode 100644 index 000000000000..b302c01763b5 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/NamespaceSummary.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record NamespaceSummary : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new NamespaceSummary type from its Protobuf-equivalent representation. + /// + internal static NamespaceSummary FromProto(ProtoDataV1Grpc.NamespaceSummary value) + { + return new NamespaceSummary { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the NamespaceSummary type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.NamespaceSummary ToProto() + { + var result = new ProtoDataV1Grpc.NamespaceSummary(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Pagination.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Pagination.cs new file mode 100644 index 000000000000..2eb1d1eeedec --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Pagination.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Pagination : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("next")] + public string? Next { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Pagination type from its Protobuf-equivalent representation. + /// + internal static Pagination FromProto(ProtoDataV1Grpc.Pagination value) + { + return new Pagination { Next = value.Next }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Pagination type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Pagination ToProto() + { + var result = new ProtoDataV1Grpc.Pagination(); + if (Next != null) + { + result.Next = Next ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryColumn.cs new file mode 100644 index 000000000000..d2621a6d87c6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryColumn.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("top_k")] + public uint? TopK { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryColumn type from its Protobuf-equivalent representation. + /// + internal static QueryColumn FromProto(ProtoDataV1Grpc.QueryColumn value) + { + return new QueryColumn + { + Values = value.Values?.ToList() ?? Enumerable.Empty(), + TopK = value.TopK, + Namespace = value.Namespace, + Filter = value.Filter != null ? Metadata.FromProto(value.Filter) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryColumn ToProto() + { + var result = new ProtoDataV1Grpc.QueryColumn(); + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (TopK != null) + { + result.TopK = TopK ?? 0; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResponse.cs new file mode 100644 index 000000000000..9242ccd094a1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("results")] + public IEnumerable? Results { get; set; } + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResponse type from its Protobuf-equivalent representation. + /// + internal static QueryResponse FromProto(ProtoDataV1Grpc.QueryResponse value) + { + return new QueryResponse + { + Results = value.Results?.Select(QueryResult.FromProto), + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResponse ToProto() + { + var result = new ProtoDataV1Grpc.QueryResponse(); + if (Results != null && Results.Any()) + { + result.Results.AddRange(Results.Select(elem => elem.ToProto())); + } + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResult.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResult.cs new file mode 100644 index 000000000000..8a0ab47d124a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/QueryResult.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResult : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResult type from its Protobuf-equivalent representation. + /// + internal static QueryResult FromProto(ProtoDataV1Grpc.QueryResult value) + { + return new QueryResult + { + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResult type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResult ToProto() + { + var result = new ProtoDataV1Grpc.QueryResult(); + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ScoredColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ScoredColumn.cs new file mode 100644 index 000000000000..a0d7fc819768 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/ScoredColumn.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ScoredColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("score")] + public float? Score { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ScoredColumn type from its Protobuf-equivalent representation. + /// + internal static ScoredColumn FromProto(ProtoDataV1Grpc.ScoredColumn value) + { + return new ScoredColumn + { + Id = value.Id, + Score = value.Score, + Values = value.Values?.ToList(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ScoredColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ScoredColumn ToProto() + { + var result = new ProtoDataV1Grpc.ScoredColumn(); + result.Id = Id; + if (Score != null) + { + result.Score = Score ?? 0.0f; + } + if (Values != null && Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UpdateResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UpdateResponse.cs new file mode 100644 index 000000000000..643f8a79e161 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UpdateResponse.cs @@ -0,0 +1,116 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record UpdateResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("updated_at")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UpdateResponse type from its Protobuf-equivalent representation. + /// + internal static UpdateResponse FromProto(ProtoDataV1Grpc.UpdateResponse value) + { + return new UpdateResponse + { + UpdatedAt = value.UpdatedAt.ToDateTime(), + IndexType = value.IndexType switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexType}"), + }, + Details = value.Details != null ? value.Details : null, + IndexTypes = value.IndexTypes.Select(type => + type switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexTypes}"), + } + ), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UpdateResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UpdateResponse ToProto() + { + var result = new ProtoDataV1Grpc.UpdateResponse(); + if (UpdatedAt != null) + { + result.UpdatedAt = WellKnownProto.Timestamp.FromDateTime( + UpdatedAt.Value.ToUniversalTime() + ); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UploadResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UploadResponse.cs new file mode 100644 index 000000000000..2409ef7d7bff --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/UploadResponse.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UploadResponse type from its Protobuf-equivalent representation. + /// + internal static UploadResponse FromProto(ProtoDataV1Grpc.UploadResponse value) + { + return new UploadResponse { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UploadResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UploadResponse ToProto() + { + var result = new ProtoDataV1Grpc.UploadResponse(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Usage.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Usage.cs new file mode 100644 index 000000000000..c6c1ccf37d4a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/Usage.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Usage : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("units")] + public uint? Units { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Usage type from its Protobuf-equivalent representation. + /// + internal static Usage FromProto(ProtoDataV1Grpc.Usage value) + { + return new Usage { Units = value.Units }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Usage type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Usage ToProto() + { + var result = new ProtoDataV1Grpc.Usage(); + if (Units != null) + { + result.Units = Units ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/README.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/README.md index 79564745a6ec..7cc184191f18 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/README.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/README.md @@ -15,9 +15,8 @@ The Seed C# library provides convenient access to the Seed APIs from C#. - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) - - [Raw Response](#raw-response) - [Additional Headers](#additional-headers) - - [Additional Query Parameters](#additional-query-parameters) + - [Forward Compatible Enums](#forward-compatible-enums) - [Contributing](#contributing) ## Requirements @@ -99,34 +98,6 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Raw Response - -Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. - -```csharp -using SeedApi; - -// Access raw response data (status code, headers, etc.) alongside the parsed response -var result = await client.Dataservice.FooAsync(...).WithRawResponse(); - -// Access the parsed data -var data = result.Data; - -// Access raw response metadata -var statusCode = result.RawResponse.StatusCode; -var headers = result.RawResponse.Headers; -var url = result.RawResponse.Url; - -// Access specific headers (case-insensitive) -if (headers.TryGetValue("X-Request-Id", out var requestId)) -{ - System.Console.WriteLine($"Request ID: {requestId}"); -} - -// For the default behavior, simply await without .WithRawResponse() -var data = await client.Dataservice.FooAsync(...); -``` - ### Additional Headers If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. @@ -143,20 +114,33 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Additional Query Parameters +### Forward Compatible Enums -If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. +This SDK uses forward-compatible enums that can handle unknown values gracefully. ```csharp -var response = await client.Dataservice.FooAsync( - ..., - new RequestOptions { - AdditionalQueryParameters = new Dictionary - { - { "custom_param", "custom-value" } - } - } -); +using SeedApi; + +// Using a built-in value +var indexType = IndexType.IndexTypeInvalid; + +// Using a custom value +var customIndexType = IndexType.FromCustom("custom-value"); + +// Using in a switch statement +switch (indexType.Value) +{ + case IndexType.Values.IndexTypeInvalid: + Console.WriteLine("IndexTypeInvalid"); + break; + default: + Console.WriteLine($"Unknown value: {indexType.Value}"); + break; +} + +// Explicit casting +string indexTypeString = (string)IndexType.IndexTypeInvalid; +IndexType indexTypeFromString = (IndexType)"INDEX_TYPE_INVALID"; ``` ## Contributing diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/data/v1/data.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/data/v1/data.proto new file mode 100644 index 000000000000..b8244b95516d --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/data/v1/data.proto @@ -0,0 +1,230 @@ +syntax = "proto3"; + +package data.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; + +option csharp_namespace = "Data.V1.Grpc"; +option go_package = "github.com/acme.co/data-go-grpc"; + +enum IndexType { + INDEX_TYPE_INVALID = 0; + INDEX_TYPE_DEFAULT = 1; + INDEX_TYPE_STRICT = 2; +} + +message IndexedData { + repeated uint32 indices = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; +} + +message Column { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct metadata = 3; + IndexedData indexed_data = 4; +} + +message ScoredColumn { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + float score = 2; + repeated float values = 3; + google.protobuf.Struct metadata = 4; + IndexedData indexed_data = 5; +} + +message UploadRequest { + repeated Column columns = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message UploadResponse { + uint32 count = 1; +} + +message DeleteRequest { + repeated string ids = 1; + bool delete_all = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; +} + +message DeleteResponse {} + +message FetchRequest { + repeated string ids = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message FetchResponse { + map columns = 1; + string namespace = 2; + optional Usage usage = 3; +} + +message ListRequest { + optional string prefix = 1; + optional uint32 limit = 2; + optional string pagination_token = 3; + string namespace = 4; +} + +message Pagination { + string next = 1; +} + +message ListElement { + string id = 1; +} + +message ListResponse { + repeated ListElement columns = 1; + optional Pagination pagination = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message QueryColumn { + repeated float values = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + uint32 top_k = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; + IndexedData indexed_data = 5; +} + +message QueryRequest { + string namespace = 1; + uint32 top_k = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct filter = 3; + bool include_values = 4; + bool include_metadata = 5; + repeated QueryColumn queries = 6 [ + deprecated = true + ]; + repeated float column = 7; + string id = 8; + IndexedData indexed_data = 9; +} + +message QueryResult { + repeated ScoredColumn matches = 1; + string namespace = 2; +} + +message QueryResponse { + repeated QueryResult results = 1 [deprecated=true]; + repeated ScoredColumn matches = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message Usage { + optional uint32 units = 1; +} + +message UpdateRequest { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2; + google.protobuf.Struct set_metadata = 3; + string namespace = 4; + IndexedData indexed_data = 5; + IndexType index_type = 6; + google.protobuf.Any details = 7; + repeated IndexType index_types = 8; +} + +message UpdateResponse { + google.protobuf.Timestamp updated_at = 1; + IndexType index_type = 2; + google.protobuf.Any details = 3; + repeated IndexType index_types = 4; +} + +message DescribeRequest { + google.protobuf.Struct filter = 1; + google.protobuf.Timestamp after = 2; +} + +message NamespaceSummary { + uint32 count = 1; +} + +message DescribeResponse { + map namespaces = 1; + uint32 dimension = 2; + float fullness = 3; + uint32 total_count = 4; +} + +service DataService { + rpc Upload(UploadRequest) returns (UploadResponse) { + option (google.api.http) = { + post: "/data" + body: "*" + }; + } + + rpc Delete(DeleteRequest) returns (DeleteResponse) { + option (google.api.http) = { + post: "/data/delete" + body: "*" + }; + } + + rpc Fetch(FetchRequest) returns (FetchResponse) { + option (google.api.http) = { + get: "/data/fetch" + }; + } + + rpc List(ListRequest) returns (ListResponse) { + option (google.api.http) = { + get: "/data/list" + }; + } + + rpc Query(QueryRequest) returns (QueryResponse) { + option (google.api.http) = { + post: "/data/query" + body: "*" + }; + } + + rpc Update(UpdateRequest) returns (UpdateResponse) { + option (google.api.http) = { + post: "/data/update" + body: "*" + }; + } + + rpc Describe(DescribeRequest) returns (DescribeResponse) { + option (google.api.http) = { + post: "/data/describe" + body: "*" + }; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/annotations.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/annotations.proto new file mode 100644 index 000000000000..8ff42098404c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/field_behavior.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/field_behavior.proto new file mode 100644 index 000000000000..128799c558db --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/http.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/http.proto new file mode 100644 index 000000000000..c8392381eb99 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/proto/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/reference.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/reference.md index bfc647a5cfb6..b5f9228a5c91 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/reference.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/reference.md @@ -1,5 +1,5 @@ # Reference -## Dataservice +## DataService
client.Dataservice.FooAsync() -> WithRawResponseTask<Dictionary<string, object?>>
@@ -25,3 +25,295 @@ await client.Dataservice.FooAsync();
+
client.Dataservice.UploadAsync(UploadRequest { ... }) -> WithRawResponseTask<UploadResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UploadAsync( + new UploadRequest + { + Columns = new List() + { + new SeedApi.Column + { + Id = "id", + Values = new List() { 1.1f }, + }, + }, + } +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UploadRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DeleteAsync(DeleteRequest { ... }) -> WithRawResponseTask<DeleteResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DeleteAsync(new DeleteRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DeleteRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DescribeAsync(DescribeRequest { ... }) -> WithRawResponseTask<DescribeResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DescribeAsync(new DescribeRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DescribeRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.FetchAsync(FetchRequest { ... }) -> WithRawResponseTask<FetchResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.FetchAsync(new FetchRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `FetchRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.ListAsync(ListRequest { ... }) -> WithRawResponseTask<ListResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.ListAsync(new ListRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ListRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.QueryAsync(QueryRequest { ... }) -> WithRawResponseTask<QueryResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `QueryRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.UpdateAsync(UpdateRequest { ... }) -> WithRawResponseTask<UpdateResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UpdateRequest` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/snippet.json b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/snippet.json index 6cef6bd0ec54..10d4d0868455 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/snippet.json +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/snippet.json @@ -12,6 +12,90 @@ "type": "csharp", "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FooAsync();\n" } + }, + { + "example_identifier": null, + "id": { + "path": "/data", + "method": "POST", + "identifier_override": "endpoint_dataservice.upload" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UploadAsync(\n new UploadRequest\n {\n Columns = new List()\n {\n new SeedApi.Column\n {\n Id = \"id\",\n Values = new List() { 1.1f },\n },\n },\n }\n);\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/delete", + "method": "POST", + "identifier_override": "endpoint_dataservice.delete" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DeleteAsync(new DeleteRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/describe", + "method": "POST", + "identifier_override": "endpoint_dataservice.describe" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DescribeAsync(new DescribeRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/fetch", + "method": "GET", + "identifier_override": "endpoint_dataservice.fetch" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FetchAsync(new FetchRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/list", + "method": "GET", + "identifier_override": "endpoint_dataservice.list" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.ListAsync(new ListRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/query", + "method": "POST", + "identifier_override": "endpoint_dataservice.query" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/update", + "method": "POST", + "identifier_override": "endpoint_dataservice.update" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UpdateAsync(new UpdateRequest { Id = \"id\" });\n" + } } ] } \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example0.cs deleted file mode 100644 index 5c845cf2e096..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example0.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example0 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example1.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example1.cs deleted file mode 100644 index dc9e849f6c96..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/Example1.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example1 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj deleted file mode 100644 index 3417db2e58e2..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - 12 - enable - enable - - - - - - \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs deleted file mode 100644 index cb111a797b9a..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using NUnit.Framework; -using SeedApi; -using WireMock.Logging; -using WireMock.Server; -using WireMock.Settings; - -namespace SeedApi.Test.Unit.MockServer; - -[SetUpFixture] -public class BaseMockServerTest -{ - protected static WireMockServer Server { get; set; } = null!; - - protected static SeedApiClient Client { get; set; } = null!; - - protected static RequestOptions RequestOptions { get; set; } = new(); - - [OneTimeSetUp] - public void GlobalSetup() - { - // Start the WireMock server - Server = WireMockServer.Start( - new WireMockServerSettings { Logger = new WireMockConsoleLogger() } - ); - - // Initialize the Client - Client = new SeedApiClient( - clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } - ); - } - - [OneTimeTearDown] - public void GlobalTeardown() - { - Server.Stop(); - Server.Dispose(); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs deleted file mode 100644 index 3a6c904371a3..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using NUnit.Framework; -using SeedApi.Test.Unit.MockServer; -using SeedApi.Test.Utils; - -namespace SeedApi.Test.Unit.MockServer.Dataservice; - -[TestFixture] -public class FooTest : BaseMockServerTest -{ - [NUnit.Framework.Test] - public async Task MockServerTest_1() - { - const string mockResponse = """ - { - "string": { - "key": "value" - } - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } - - [NUnit.Framework.Test] - public async Task MockServerTest_2() - { - const string mockResponse = """ - { - "key": "value" - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/ProtoAnyMapper.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/ProtoAnyMapper.cs new file mode 100644 index 000000000000..5c55aa625072 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/ProtoAnyMapper.cs @@ -0,0 +1,28 @@ +using global::System.Reflection; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi.Core; + +public static class ProtoAnyMapper +{ + public static Any? ToProto(object? value) + { + if (value is null) + { + return null; + } + var toProtoMethod = value + .GetType() + .GetMethod("ToProto", BindingFlags.Instance | BindingFlags.NonPublic); + if (toProtoMethod is null) + { + throw new InvalidOperationException( + $"Type {value.GetType()} does not have a ToProto method" + ); + } + var protoValue = toProtoMethod.Invoke(value, null); + return WellKnownProto.Any.Pack((IMessage)protoValue); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/ClientOptions.cs index 837716a987f2..6cf621e7bb97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/ClientOptions.cs @@ -1,3 +1,4 @@ +using Grpc.Net.Client; using SeedApi.Core; namespace SeedApi; @@ -66,6 +67,17 @@ public partial class ClientOptions #endif } = TimeSpan.FromSeconds(30); + /// + /// The options used for gRPC client endpoints. + /// + public GrpcChannelOptions? GrpcOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + /// /// Clones this and returns a new instance /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs new file mode 100644 index 000000000000..4fb311172634 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs @@ -0,0 +1,63 @@ +using Grpc.Core; +using SeedApi.Core; + +namespace SeedApi; + +public partial class GrpcRequestOptions +{ + /// + /// The maximum number of retry attempts. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Options for write operations. + /// + public WriteOptions? WriteOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Client-side call credentials. Provide authorization with per-call granularity. + /// + public CallCredentials? CallCredentials { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with this particular request. + /// Headers with matching keys will be overwritten by headers set on the client options. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new List>(); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawClient.cs index d42791fcc0e0..3e191c04dbe4 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawClient.cs @@ -20,6 +20,13 @@ internal partial class RawClient(ClientOptions clientOptions) #endif internal int BaseRetryDelay { get; set; } = 1000; + private readonly Lazy _grpc = new(() => new RawGrpcClient(clientOptions)); + + /// + /// The gRPC client used to make requests. + /// + public RawGrpcClient Grpc => _grpc.Value; + /// /// The client options applied on every request. /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs new file mode 100644 index 000000000000..2326d6b36f8c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs @@ -0,0 +1,61 @@ +using Grpc.Core; +using Grpc.Net.Client; + +namespace SeedApi.Core; + +/// +/// Utility class for making gRPC requests to the API. +/// +internal class RawGrpcClient +{ + /// + /// The gRPC channel used to make requests. + /// + public readonly GrpcChannel Channel; + + private readonly ClientOptions _clientOptions; + + public RawGrpcClient(ClientOptions clientOptions) + { + _clientOptions = clientOptions; + + var grpcOptions = PrepareGrpcChannelOptions(); + Channel = grpcOptions is not null + ? GrpcChannel.ForAddress(_clientOptions.BaseUrl, grpcOptions) + : GrpcChannel.ForAddress(_clientOptions.BaseUrl); + } + + /// + /// Creates CallOptions for a gRPC request with the provided metadata, timeout, and credentials. + /// Metadata (headers) should be built at the endpoint level before calling this method. + /// + public CallOptions CreateCallOptions( + global::Grpc.Core.Metadata metadata, + GrpcRequestOptions options, + CancellationToken cancellationToken = default + ) + { + var timeout = options.Timeout ?? _clientOptions.Timeout; + var deadline = DateTime.UtcNow.Add(timeout); + return new CallOptions( + metadata, + deadline, + cancellationToken, + options.WriteOptions, + null, + options.CallCredentials + ); + } + + private GrpcChannelOptions? PrepareGrpcChannelOptions() + { + var grpcChannelOptions = _clientOptions.GrpcOptions; + if (grpcChannelOptions is null) + { + return null; + } + grpcChannelOptions.HttpClient ??= _clientOptions.HttpClient; + grpcChannelOptions.MaxRetryAttempts ??= _clientOptions.MaxRetries; + return grpcChannelOptions; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/DataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/DataserviceClient.cs index 984e4e741a6b..d7a55705956f 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/DataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/DataserviceClient.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using Data.V1.Grpc; +using Grpc.Core; using SeedApi.Core; namespace SeedApi; @@ -7,9 +9,15 @@ public partial class DataserviceClient : IDataserviceClient { private readonly RawClient _client; + private readonly RawGrpcClient _grpc; + + private DataService.DataServiceClient _dataService; + internal DataserviceClient(RawClient client) { _client = client; + _grpc = _client.Grpc; + _dataService = new DataService.DataServiceClient(_grpc.Channel); } private async Task>> FooAsyncCore( @@ -90,4 +98,422 @@ internal DataserviceClient(RawClient client) FooAsyncCore(options, cancellationToken) ); } + + /// + /// await client.Dataservice.UploadAsync( + /// new UploadRequest + /// { + /// Columns = new List<SeedApi.Column>() + /// { + /// new SeedApi.Column + /// { + /// Id = "id", + /// Values = new List<float>() { 1.1f }, + /// }, + /// }, + /// } + /// ); + /// + public async Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UploadAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UploadResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.DeleteAsync(new DeleteRequest()); + /// + public async Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DeleteAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DeleteResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.DescribeAsync(new DescribeRequest()); + /// + public async Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DescribeAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DescribeResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.FetchAsync(new FetchRequest()); + /// + public async Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.FetchAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return FetchResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.ListAsync(new ListRequest()); + /// + public async Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.ListAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return ListResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); + /// + public async Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.QueryAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return QueryResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); + /// + public async Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UpdateAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UpdateResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/IDataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/IDataserviceClient.cs index 790e52ff98cd..4a7de66ccb27 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/IDataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/IDataserviceClient.cs @@ -6,4 +6,46 @@ public partial interface IDataserviceClient RequestOptions? options = null, CancellationToken cancellationToken = default ); + + Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DeleteRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DeleteRequest.cs new file mode 100644 index 000000000000..6226129bb2dc --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DeleteRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteRequest +{ + [JsonPropertyName("ids")] + public IEnumerable? Ids { get; set; } + + [JsonPropertyName("delete_all")] + public bool? DeleteAll { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + /// + /// Maps the DeleteRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DeleteRequest ToProto() + { + var result = new Proto.DeleteRequest(); + if (Ids != null && Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (DeleteAll != null) + { + result.DeleteAll = DeleteAll ?? false; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DescribeRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DescribeRequest.cs new file mode 100644 index 000000000000..fcc72788bd2c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/DescribeRequest.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record DescribeRequest +{ + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("after")] + public DateTime? After { get; set; } + + /// + /// Maps the DescribeRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DescribeRequest ToProto() + { + var result = new Proto.DescribeRequest(); + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (After != null) + { + result.After = WellKnownProto.Timestamp.FromDateTime(After.Value.ToUniversalTime()); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/FetchRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/FetchRequest.cs new file mode 100644 index 000000000000..3ca54d9f8be1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/FetchRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchRequest +{ + [JsonIgnore] + public IEnumerable Ids { get; set; } = new List(); + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the FetchRequest type into its Protobuf-equivalent representation. + /// + internal Proto.FetchRequest ToProto() + { + var result = new Proto.FetchRequest(); + if (Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/ListRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/ListRequest.cs new file mode 100644 index 000000000000..fd26665984e4 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/ListRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListRequest +{ + [JsonIgnore] + public string? Prefix { get; set; } + + [JsonIgnore] + public uint? Limit { get; set; } + + [JsonIgnore] + public string? PaginationToken { get; set; } + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the ListRequest type into its Protobuf-equivalent representation. + /// + internal Proto.ListRequest ToProto() + { + var result = new Proto.ListRequest(); + if (Prefix != null) + { + result.Prefix = Prefix ?? ""; + } + if (Limit != null) + { + result.Limit = Limit ?? 0; + } + if (PaginationToken != null) + { + result.PaginationToken = PaginationToken ?? ""; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/QueryRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/QueryRequest.cs new file mode 100644 index 000000000000..0586a3c144b2 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/QueryRequest.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryRequest +{ + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("top_k")] + public required uint TopK { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("include_values")] + public bool? IncludeValues { get; set; } + + [JsonPropertyName("include_metadata")] + public bool? IncludeMetadata { get; set; } + + [JsonPropertyName("queries")] + public IEnumerable? Queries { get; set; } + + [JsonPropertyName("column")] + public IEnumerable? Column { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + /// + /// Maps the QueryRequest type into its Protobuf-equivalent representation. + /// + internal Proto.QueryRequest ToProto() + { + var result = new Proto.QueryRequest(); + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + result.TopK = TopK; + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IncludeValues != null) + { + result.IncludeValues = IncludeValues ?? false; + } + if (IncludeMetadata != null) + { + result.IncludeMetadata = IncludeMetadata ?? false; + } + if (Queries != null && Queries.Any()) + { + result.Queries.AddRange(Queries.Select(elem => elem.ToProto())); + } + if (Column != null && Column.Any()) + { + result.Column.AddRange(Column); + } + if (Id != null) + { + result.Id = Id ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UpdateRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UpdateRequest.cs new file mode 100644 index 000000000000..40238253ea5e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UpdateRequest.cs @@ -0,0 +1,100 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UpdateRequest +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonPropertyName("set_metadata")] + public Metadata? SetMetadata { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + /// + /// Maps the UpdateRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UpdateRequest ToProto() + { + var result = new Proto.UpdateRequest(); + result.Id = Id; + if (Values != null && Values.Any()) + { + result.Values.AddRange(Values); + } + if (SetMetadata != null) + { + result.SetMetadata = SetMetadata.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UploadRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UploadRequest.cs new file mode 100644 index 000000000000..b8b1d3749c34 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Requests/UploadRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadRequest +{ + [JsonPropertyName("columns")] + public IEnumerable Columns { get; set; } = new List(); + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + /// + /// Maps the UploadRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UploadRequest ToProto() + { + var result = new Proto.UploadRequest(); + if (Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs new file mode 100644 index 000000000000..efdca475318f --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs @@ -0,0 +1,67 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct IndexType : IStringEnum +{ + public static readonly IndexType IndexTypeInvalid = new(Values.IndexTypeInvalid); + + public static readonly IndexType IndexTypeDefault = new(Values.IndexTypeDefault); + + public static readonly IndexType IndexTypeStrict = new(Values.IndexTypeStrict); + + public IndexType(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static IndexType FromCustom(string value) + { + return new IndexType(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(IndexType value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(IndexType value1, string value2) => !value1.Value.Equals(value2); + + public static explicit operator string(IndexType value) => value.Value; + + public static explicit operator IndexType(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string IndexTypeInvalid = "INDEX_TYPE_INVALID"; + + public const string IndexTypeDefault = "INDEX_TYPE_DEFAULT"; + + public const string IndexTypeStrict = "INDEX_TYPE_STRICT"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApi.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApi.csproj index 4bac237048e3..70240c028c83 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApi.csproj +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/SeedApi.csproj @@ -44,6 +44,39 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Column.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Column.cs new file mode 100644 index 000000000000..ef225e7518ab --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Column.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Column : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Column type from its Protobuf-equivalent representation. + /// + internal static Column FromProto(ProtoDataV1Grpc.Column value) + { + return new Column + { + Id = value.Id, + Values = value.Values?.ToList() ?? Enumerable.Empty(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Column type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Column ToProto() + { + var result = new ProtoDataV1Grpc.Column(); + result.Id = Id; + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DeleteResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DeleteResponse.cs new file mode 100644 index 000000000000..2c72cc2beea7 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DeleteResponse.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DeleteResponse type from its Protobuf-equivalent representation. + /// + internal static DeleteResponse FromProto(ProtoDataV1Grpc.DeleteResponse value) + { + return new DeleteResponse(); + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DeleteResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DeleteResponse ToProto() + { + return new ProtoDataV1Grpc.DeleteResponse(); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DescribeResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DescribeResponse.cs new file mode 100644 index 000000000000..be7b2a6f4b01 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/DescribeResponse.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DescribeResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("namespaces")] + public Dictionary? Namespaces { get; set; } + + [JsonPropertyName("dimension")] + public uint? Dimension { get; set; } + + [JsonPropertyName("fullness")] + public float? Fullness { get; set; } + + [JsonPropertyName("total_count")] + public uint? TotalCount { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DescribeResponse type from its Protobuf-equivalent representation. + /// + internal static DescribeResponse FromProto(ProtoDataV1Grpc.DescribeResponse value) + { + return new DescribeResponse + { + Namespaces = value.Namespaces?.ToDictionary( + kvp => kvp.Key, + kvp => NamespaceSummary.FromProto(kvp.Value) + ), + Dimension = value.Dimension, + Fullness = value.Fullness, + TotalCount = value.TotalCount, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DescribeResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DescribeResponse ToProto() + { + var result = new ProtoDataV1Grpc.DescribeResponse(); + if (Namespaces != null && Namespaces.Any()) + { + foreach (var kvp in Namespaces) + { + result.Namespaces.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Dimension != null) + { + result.Dimension = Dimension ?? 0; + } + if (Fullness != null) + { + result.Fullness = Fullness ?? 0.0f; + } + if (TotalCount != null) + { + result.TotalCount = TotalCount ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FetchResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FetchResponse.cs new file mode 100644 index 000000000000..0558b91da66e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FetchResponse.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public Dictionary? Columns { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new FetchResponse type from its Protobuf-equivalent representation. + /// + internal static FetchResponse FromProto(ProtoDataV1Grpc.FetchResponse value) + { + return new FetchResponse + { + Columns = value.Columns?.ToDictionary( + kvp => kvp.Key, + kvp => Column.FromProto(kvp.Value) + ), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the FetchResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.FetchResponse ToProto() + { + var result = new ProtoDataV1Grpc.FetchResponse(); + if (Columns != null && Columns.Any()) + { + foreach (var kvp in Columns) + { + result.Columns.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs new file mode 100644 index 000000000000..38cc250adbcf --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs @@ -0,0 +1,95 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct FieldBehavior : IStringEnum +{ + public static readonly FieldBehavior FieldBehaviorUnspecified = new( + Values.FieldBehaviorUnspecified + ); + + public static readonly FieldBehavior Optional = new(Values.Optional); + + public static readonly FieldBehavior Required = new(Values.Required); + + public static readonly FieldBehavior OutputOnly = new(Values.OutputOnly); + + public static readonly FieldBehavior InputOnly = new(Values.InputOnly); + + public static readonly FieldBehavior Immutable = new(Values.Immutable); + + public static readonly FieldBehavior UnorderedList = new(Values.UnorderedList); + + public static readonly FieldBehavior NonEmptyDefault = new(Values.NonEmptyDefault); + + public static readonly FieldBehavior Identifier = new(Values.Identifier); + + public FieldBehavior(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static FieldBehavior FromCustom(string value) + { + return new FieldBehavior(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(FieldBehavior value1, string value2) => + value1.Value.Equals(value2); + + public static bool operator !=(FieldBehavior value1, string value2) => + !value1.Value.Equals(value2); + + public static explicit operator string(FieldBehavior value) => value.Value; + + public static explicit operator FieldBehavior(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string FieldBehaviorUnspecified = "FIELD_BEHAVIOR_UNSPECIFIED"; + + public const string Optional = "OPTIONAL"; + + public const string Required = "REQUIRED"; + + public const string OutputOnly = "OUTPUT_ONLY"; + + public const string InputOnly = "INPUT_ONLY"; + + public const string Immutable = "IMMUTABLE"; + + public const string UnorderedList = "UNORDERED_LIST"; + + public const string NonEmptyDefault = "NON_EMPTY_DEFAULT"; + + public const string Identifier = "IDENTIFIER"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/IndexedData.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/IndexedData.cs new file mode 100644 index 000000000000..4cf44e0ced78 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/IndexedData.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record IndexedData : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("indices")] + public IEnumerable Indices { get; set; } = new List(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new IndexedData type from its Protobuf-equivalent representation. + /// + internal static IndexedData FromProto(ProtoDataV1Grpc.IndexedData value) + { + return new IndexedData + { + Indices = value.Indices?.ToList() ?? Enumerable.Empty(), + Values = value.Values?.ToList() ?? Enumerable.Empty(), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the IndexedData type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.IndexedData ToProto() + { + var result = new ProtoDataV1Grpc.IndexedData(); + if (Indices.Any()) + { + result.Indices.AddRange(Indices); + } + if (Values.Any()) + { + result.Values.AddRange(Values); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListElement.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListElement.cs new file mode 100644 index 000000000000..ff0e19b9e9a9 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListElement.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListElement : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListElement type from its Protobuf-equivalent representation. + /// + internal static ListElement FromProto(ProtoDataV1Grpc.ListElement value) + { + return new ListElement { Id = value.Id }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListElement type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListElement ToProto() + { + var result = new ProtoDataV1Grpc.ListElement(); + if (Id != null) + { + result.Id = Id ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListResponse.cs new file mode 100644 index 000000000000..72b63f8287fd --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ListResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public IEnumerable? Columns { get; set; } + + [JsonPropertyName("pagination")] + public Pagination? Pagination { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListResponse type from its Protobuf-equivalent representation. + /// + internal static ListResponse FromProto(ProtoDataV1Grpc.ListResponse value) + { + return new ListResponse + { + Columns = value.Columns?.Select(ListElement.FromProto), + Pagination = value.Pagination != null ? Pagination.FromProto(value.Pagination) : null, + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListResponse ToProto() + { + var result = new ProtoDataV1Grpc.ListResponse(); + if (Columns != null && Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Pagination != null) + { + result.Pagination = Pagination.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Metadata.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Metadata.cs new file mode 100644 index 000000000000..a767a7f0bdd6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Metadata.cs @@ -0,0 +1,39 @@ +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } + + internal static Metadata FromProto(WellKnownProto.Struct value) + { + var result = new Metadata(); + foreach (var kvp in value.Fields) + { + result[kvp.Key] = kvp.Value != null ? MetadataValue.FromProto(kvp.Value) : null; + } + return result; + } + + internal WellKnownProto.Struct ToProto() + { + var result = new WellKnownProto.Struct(); + foreach (var kvp in this) + { + result.Fields[kvp.Key] = kvp.Value?.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/MetadataValue.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/MetadataValue.cs new file mode 100644 index 000000000000..51b7b46595b3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/MetadataValue.cs @@ -0,0 +1,91 @@ +using OneOf; +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class MetadataValue( + OneOf, Metadata> value +) : OneOfBase, Metadata>(value) +{ + internal static MetadataValue? FromProto(WellKnownProto.Value value) + { + return value.KindCase switch + { + WellKnownProto.Value.KindOneofCase.StringValue => value.StringValue, + WellKnownProto.Value.KindOneofCase.NumberValue => value.NumberValue, + WellKnownProto.Value.KindOneofCase.BoolValue => value.BoolValue, + WellKnownProto.Value.KindOneofCase.ListValue => value + .ListValue.Values.Select(FromProto) + .ToList(), + WellKnownProto.Value.KindOneofCase.StructValue => Metadata.FromProto(value.StructValue), + _ => null, + }; + } + + internal WellKnownProto.Value ToProto() + { + return Match( + WellKnownProto.Value.ForString, + WellKnownProto.Value.ForNumber, + WellKnownProto.Value.ForBool, + list => new WellKnownProto.Value + { + ListValue = new WellKnownProto.ListValue + { + Values = { list.Select(item => item?.ToProto()) }, + }, + }, + nested => new WellKnownProto.Value { StructValue = nested.ToProto() } + ); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } + + public static implicit operator MetadataValue(string value) => new(value); + + public static implicit operator MetadataValue(bool value) => new(value); + + public static implicit operator MetadataValue(double value) => new(value); + + public static implicit operator MetadataValue(Metadata value) => new(value); + + public static implicit operator MetadataValue(MetadataValue?[] value) => new(value); + + public static implicit operator MetadataValue(List value) => new(value); + + public static implicit operator MetadataValue(string[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(bool[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(bool?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/NamespaceSummary.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/NamespaceSummary.cs new file mode 100644 index 000000000000..b302c01763b5 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/NamespaceSummary.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record NamespaceSummary : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new NamespaceSummary type from its Protobuf-equivalent representation. + /// + internal static NamespaceSummary FromProto(ProtoDataV1Grpc.NamespaceSummary value) + { + return new NamespaceSummary { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the NamespaceSummary type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.NamespaceSummary ToProto() + { + var result = new ProtoDataV1Grpc.NamespaceSummary(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Pagination.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Pagination.cs new file mode 100644 index 000000000000..2eb1d1eeedec --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Pagination.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Pagination : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("next")] + public string? Next { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Pagination type from its Protobuf-equivalent representation. + /// + internal static Pagination FromProto(ProtoDataV1Grpc.Pagination value) + { + return new Pagination { Next = value.Next }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Pagination type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Pagination ToProto() + { + var result = new ProtoDataV1Grpc.Pagination(); + if (Next != null) + { + result.Next = Next ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryColumn.cs new file mode 100644 index 000000000000..d2621a6d87c6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryColumn.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("top_k")] + public uint? TopK { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryColumn type from its Protobuf-equivalent representation. + /// + internal static QueryColumn FromProto(ProtoDataV1Grpc.QueryColumn value) + { + return new QueryColumn + { + Values = value.Values?.ToList() ?? Enumerable.Empty(), + TopK = value.TopK, + Namespace = value.Namespace, + Filter = value.Filter != null ? Metadata.FromProto(value.Filter) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryColumn ToProto() + { + var result = new ProtoDataV1Grpc.QueryColumn(); + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (TopK != null) + { + result.TopK = TopK ?? 0; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResponse.cs new file mode 100644 index 000000000000..9242ccd094a1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("results")] + public IEnumerable? Results { get; set; } + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResponse type from its Protobuf-equivalent representation. + /// + internal static QueryResponse FromProto(ProtoDataV1Grpc.QueryResponse value) + { + return new QueryResponse + { + Results = value.Results?.Select(QueryResult.FromProto), + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResponse ToProto() + { + var result = new ProtoDataV1Grpc.QueryResponse(); + if (Results != null && Results.Any()) + { + result.Results.AddRange(Results.Select(elem => elem.ToProto())); + } + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResult.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResult.cs new file mode 100644 index 000000000000..8a0ab47d124a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/QueryResult.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResult : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResult type from its Protobuf-equivalent representation. + /// + internal static QueryResult FromProto(ProtoDataV1Grpc.QueryResult value) + { + return new QueryResult + { + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResult type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResult ToProto() + { + var result = new ProtoDataV1Grpc.QueryResult(); + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ScoredColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ScoredColumn.cs new file mode 100644 index 000000000000..a0d7fc819768 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/ScoredColumn.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ScoredColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("score")] + public float? Score { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ScoredColumn type from its Protobuf-equivalent representation. + /// + internal static ScoredColumn FromProto(ProtoDataV1Grpc.ScoredColumn value) + { + return new ScoredColumn + { + Id = value.Id, + Score = value.Score, + Values = value.Values?.ToList(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ScoredColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ScoredColumn ToProto() + { + var result = new ProtoDataV1Grpc.ScoredColumn(); + result.Id = Id; + if (Score != null) + { + result.Score = Score ?? 0.0f; + } + if (Values != null && Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UpdateResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UpdateResponse.cs new file mode 100644 index 000000000000..643f8a79e161 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UpdateResponse.cs @@ -0,0 +1,116 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record UpdateResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("updated_at")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UpdateResponse type from its Protobuf-equivalent representation. + /// + internal static UpdateResponse FromProto(ProtoDataV1Grpc.UpdateResponse value) + { + return new UpdateResponse + { + UpdatedAt = value.UpdatedAt.ToDateTime(), + IndexType = value.IndexType switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexType}"), + }, + Details = value.Details != null ? value.Details : null, + IndexTypes = value.IndexTypes.Select(type => + type switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexTypes}"), + } + ), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UpdateResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UpdateResponse ToProto() + { + var result = new ProtoDataV1Grpc.UpdateResponse(); + if (UpdatedAt != null) + { + result.UpdatedAt = WellKnownProto.Timestamp.FromDateTime( + UpdatedAt.Value.ToUniversalTime() + ); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UploadResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UploadResponse.cs new file mode 100644 index 000000000000..2409ef7d7bff --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/UploadResponse.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UploadResponse type from its Protobuf-equivalent representation. + /// + internal static UploadResponse FromProto(ProtoDataV1Grpc.UploadResponse value) + { + return new UploadResponse { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UploadResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UploadResponse ToProto() + { + var result = new ProtoDataV1Grpc.UploadResponse(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Usage.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Usage.cs new file mode 100644 index 000000000000..c6c1ccf37d4a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/Usage.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Usage : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("units")] + public uint? Units { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Usage type from its Protobuf-equivalent representation. + /// + internal static Usage FromProto(ProtoDataV1Grpc.Usage value) + { + return new Usage { Units = value.Units }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Usage type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Usage ToProto() + { + var result = new ProtoDataV1Grpc.Usage(); + if (Units != null) + { + result.Units = Units ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/README.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/README.md index fc6706bc0bca..bc4d312ebd54 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/README.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/README.md @@ -15,9 +15,8 @@ The Seed C# library provides convenient access to the Seed APIs from C#. - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) - - [Raw Response](#raw-response) - [Additional Headers](#additional-headers) - - [Additional Query Parameters](#additional-query-parameters) + - [Forward Compatible Enums](#forward-compatible-enums) - [Contributing](#contributing) ## Requirements @@ -99,34 +98,6 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Raw Response - -Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. - -```csharp -using SeedApi; - -// Access raw response data (status code, headers, etc.) alongside the parsed response -var result = await client.Dataservice.FooAsync(...).WithRawResponse(); - -// Access the parsed data -var data = result.Data; - -// Access raw response metadata -var statusCode = result.RawResponse.StatusCode; -var headers = result.RawResponse.Headers; -var url = result.RawResponse.Url; - -// Access specific headers (case-insensitive) -if (headers.TryGetValue("X-Request-Id", out var requestId)) -{ - System.Console.WriteLine($"Request ID: {requestId}"); -} - -// For the default behavior, simply await without .WithRawResponse() -var data = await client.Dataservice.FooAsync(...); -``` - ### Additional Headers If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. @@ -143,20 +114,33 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Additional Query Parameters +### Forward Compatible Enums -If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. +This SDK uses forward-compatible enums that can handle unknown values gracefully. ```csharp -var response = await client.Dataservice.FooAsync( - ..., - new RequestOptions { - AdditionalQueryParameters = new Dictionary - { - { "custom_param", "custom-value" } - } - } -); +using SeedApi; + +// Using a built-in value +var indexType = IndexType.IndexTypeInvalid; + +// Using a custom value +var customIndexType = IndexType.FromCustom("custom-value"); + +// Using in a switch statement +switch (indexType.Value) +{ + case IndexType.Values.IndexTypeInvalid: + Console.WriteLine("IndexTypeInvalid"); + break; + default: + Console.WriteLine($"Unknown value: {indexType.Value}"); + break; +} + +// Explicit casting +string indexTypeString = (string)IndexType.IndexTypeInvalid; +IndexType indexTypeFromString = (IndexType)"INDEX_TYPE_INVALID"; ``` ## Contributing diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/data/v1/data.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/data/v1/data.proto new file mode 100644 index 000000000000..b8244b95516d --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/data/v1/data.proto @@ -0,0 +1,230 @@ +syntax = "proto3"; + +package data.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; + +option csharp_namespace = "Data.V1.Grpc"; +option go_package = "github.com/acme.co/data-go-grpc"; + +enum IndexType { + INDEX_TYPE_INVALID = 0; + INDEX_TYPE_DEFAULT = 1; + INDEX_TYPE_STRICT = 2; +} + +message IndexedData { + repeated uint32 indices = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; +} + +message Column { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct metadata = 3; + IndexedData indexed_data = 4; +} + +message ScoredColumn { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + float score = 2; + repeated float values = 3; + google.protobuf.Struct metadata = 4; + IndexedData indexed_data = 5; +} + +message UploadRequest { + repeated Column columns = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message UploadResponse { + uint32 count = 1; +} + +message DeleteRequest { + repeated string ids = 1; + bool delete_all = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; +} + +message DeleteResponse {} + +message FetchRequest { + repeated string ids = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message FetchResponse { + map columns = 1; + string namespace = 2; + optional Usage usage = 3; +} + +message ListRequest { + optional string prefix = 1; + optional uint32 limit = 2; + optional string pagination_token = 3; + string namespace = 4; +} + +message Pagination { + string next = 1; +} + +message ListElement { + string id = 1; +} + +message ListResponse { + repeated ListElement columns = 1; + optional Pagination pagination = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message QueryColumn { + repeated float values = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + uint32 top_k = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; + IndexedData indexed_data = 5; +} + +message QueryRequest { + string namespace = 1; + uint32 top_k = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct filter = 3; + bool include_values = 4; + bool include_metadata = 5; + repeated QueryColumn queries = 6 [ + deprecated = true + ]; + repeated float column = 7; + string id = 8; + IndexedData indexed_data = 9; +} + +message QueryResult { + repeated ScoredColumn matches = 1; + string namespace = 2; +} + +message QueryResponse { + repeated QueryResult results = 1 [deprecated=true]; + repeated ScoredColumn matches = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message Usage { + optional uint32 units = 1; +} + +message UpdateRequest { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2; + google.protobuf.Struct set_metadata = 3; + string namespace = 4; + IndexedData indexed_data = 5; + IndexType index_type = 6; + google.protobuf.Any details = 7; + repeated IndexType index_types = 8; +} + +message UpdateResponse { + google.protobuf.Timestamp updated_at = 1; + IndexType index_type = 2; + google.protobuf.Any details = 3; + repeated IndexType index_types = 4; +} + +message DescribeRequest { + google.protobuf.Struct filter = 1; + google.protobuf.Timestamp after = 2; +} + +message NamespaceSummary { + uint32 count = 1; +} + +message DescribeResponse { + map namespaces = 1; + uint32 dimension = 2; + float fullness = 3; + uint32 total_count = 4; +} + +service DataService { + rpc Upload(UploadRequest) returns (UploadResponse) { + option (google.api.http) = { + post: "/data" + body: "*" + }; + } + + rpc Delete(DeleteRequest) returns (DeleteResponse) { + option (google.api.http) = { + post: "/data/delete" + body: "*" + }; + } + + rpc Fetch(FetchRequest) returns (FetchResponse) { + option (google.api.http) = { + get: "/data/fetch" + }; + } + + rpc List(ListRequest) returns (ListResponse) { + option (google.api.http) = { + get: "/data/list" + }; + } + + rpc Query(QueryRequest) returns (QueryResponse) { + option (google.api.http) = { + post: "/data/query" + body: "*" + }; + } + + rpc Update(UpdateRequest) returns (UpdateResponse) { + option (google.api.http) = { + post: "/data/update" + body: "*" + }; + } + + rpc Describe(DescribeRequest) returns (DescribeResponse) { + option (google.api.http) = { + post: "/data/describe" + body: "*" + }; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/annotations.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/annotations.proto new file mode 100644 index 000000000000..8ff42098404c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/field_behavior.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/field_behavior.proto new file mode 100644 index 000000000000..128799c558db --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/http.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/http.proto new file mode 100644 index 000000000000..c8392381eb99 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/proto/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/reference.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/reference.md index bfc647a5cfb6..b5f9228a5c91 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/reference.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/reference.md @@ -1,5 +1,5 @@ # Reference -## Dataservice +## DataService
client.Dataservice.FooAsync() -> WithRawResponseTask<Dictionary<string, object?>>
@@ -25,3 +25,295 @@ await client.Dataservice.FooAsync();
+
client.Dataservice.UploadAsync(UploadRequest { ... }) -> WithRawResponseTask<UploadResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UploadAsync( + new UploadRequest + { + Columns = new List() + { + new SeedApi.Column + { + Id = "id", + Values = new List() { 1.1f }, + }, + }, + } +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UploadRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DeleteAsync(DeleteRequest { ... }) -> WithRawResponseTask<DeleteResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DeleteAsync(new DeleteRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DeleteRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DescribeAsync(DescribeRequest { ... }) -> WithRawResponseTask<DescribeResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DescribeAsync(new DescribeRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DescribeRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.FetchAsync(FetchRequest { ... }) -> WithRawResponseTask<FetchResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.FetchAsync(new FetchRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `FetchRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.ListAsync(ListRequest { ... }) -> WithRawResponseTask<ListResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.ListAsync(new ListRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ListRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.QueryAsync(QueryRequest { ... }) -> WithRawResponseTask<QueryResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `QueryRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.UpdateAsync(UpdateRequest { ... }) -> WithRawResponseTask<UpdateResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UpdateRequest` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/snippet.json b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/snippet.json index 6cef6bd0ec54..10d4d0868455 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/snippet.json +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/snippet.json @@ -12,6 +12,90 @@ "type": "csharp", "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FooAsync();\n" } + }, + { + "example_identifier": null, + "id": { + "path": "/data", + "method": "POST", + "identifier_override": "endpoint_dataservice.upload" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UploadAsync(\n new UploadRequest\n {\n Columns = new List()\n {\n new SeedApi.Column\n {\n Id = \"id\",\n Values = new List() { 1.1f },\n },\n },\n }\n);\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/delete", + "method": "POST", + "identifier_override": "endpoint_dataservice.delete" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DeleteAsync(new DeleteRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/describe", + "method": "POST", + "identifier_override": "endpoint_dataservice.describe" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DescribeAsync(new DescribeRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/fetch", + "method": "GET", + "identifier_override": "endpoint_dataservice.fetch" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FetchAsync(new FetchRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/list", + "method": "GET", + "identifier_override": "endpoint_dataservice.list" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.ListAsync(new ListRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/query", + "method": "POST", + "identifier_override": "endpoint_dataservice.query" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/update", + "method": "POST", + "identifier_override": "endpoint_dataservice.update" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UpdateAsync(new UpdateRequest { Id = \"id\" });\n" + } } ] } \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example0.cs deleted file mode 100644 index 5c845cf2e096..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example0.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example0 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example1.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example1.cs deleted file mode 100644 index dc9e849f6c96..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/Example1.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example1 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj deleted file mode 100644 index 3417db2e58e2..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - 12 - enable - enable - - - - - - \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs deleted file mode 100644 index cb111a797b9a..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using NUnit.Framework; -using SeedApi; -using WireMock.Logging; -using WireMock.Server; -using WireMock.Settings; - -namespace SeedApi.Test.Unit.MockServer; - -[SetUpFixture] -public class BaseMockServerTest -{ - protected static WireMockServer Server { get; set; } = null!; - - protected static SeedApiClient Client { get; set; } = null!; - - protected static RequestOptions RequestOptions { get; set; } = new(); - - [OneTimeSetUp] - public void GlobalSetup() - { - // Start the WireMock server - Server = WireMockServer.Start( - new WireMockServerSettings { Logger = new WireMockConsoleLogger() } - ); - - // Initialize the Client - Client = new SeedApiClient( - clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } - ); - } - - [OneTimeTearDown] - public void GlobalTeardown() - { - Server.Stop(); - Server.Dispose(); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs deleted file mode 100644 index 3a6c904371a3..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using NUnit.Framework; -using SeedApi.Test.Unit.MockServer; -using SeedApi.Test.Utils; - -namespace SeedApi.Test.Unit.MockServer.Dataservice; - -[TestFixture] -public class FooTest : BaseMockServerTest -{ - [NUnit.Framework.Test] - public async Task MockServerTest_1() - { - const string mockResponse = """ - { - "string": { - "key": "value" - } - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } - - [NUnit.Framework.Test] - public async Task MockServerTest_2() - { - const string mockResponse = """ - { - "key": "value" - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/ProtoAnyMapper.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/ProtoAnyMapper.cs new file mode 100644 index 000000000000..5c55aa625072 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/ProtoAnyMapper.cs @@ -0,0 +1,28 @@ +using global::System.Reflection; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi.Core; + +public static class ProtoAnyMapper +{ + public static Any? ToProto(object? value) + { + if (value is null) + { + return null; + } + var toProtoMethod = value + .GetType() + .GetMethod("ToProto", BindingFlags.Instance | BindingFlags.NonPublic); + if (toProtoMethod is null) + { + throw new InvalidOperationException( + $"Type {value.GetType()} does not have a ToProto method" + ); + } + var protoValue = toProtoMethod.Invoke(value, null); + return WellKnownProto.Any.Pack((IMessage)protoValue); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/ClientOptions.cs index 837716a987f2..6cf621e7bb97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/ClientOptions.cs @@ -1,3 +1,4 @@ +using Grpc.Net.Client; using SeedApi.Core; namespace SeedApi; @@ -66,6 +67,17 @@ public partial class ClientOptions #endif } = TimeSpan.FromSeconds(30); + /// + /// The options used for gRPC client endpoints. + /// + public GrpcChannelOptions? GrpcOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + /// /// Clones this and returns a new instance /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/GrpcRequestOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/GrpcRequestOptions.cs new file mode 100644 index 000000000000..4fb311172634 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/Public/GrpcRequestOptions.cs @@ -0,0 +1,63 @@ +using Grpc.Core; +using SeedApi.Core; + +namespace SeedApi; + +public partial class GrpcRequestOptions +{ + /// + /// The maximum number of retry attempts. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Options for write operations. + /// + public WriteOptions? WriteOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Client-side call credentials. Provide authorization with per-call granularity. + /// + public CallCredentials? CallCredentials { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with this particular request. + /// Headers with matching keys will be overwritten by headers set on the client options. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new List>(); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawClient.cs index d42791fcc0e0..3e191c04dbe4 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawClient.cs @@ -20,6 +20,13 @@ internal partial class RawClient(ClientOptions clientOptions) #endif internal int BaseRetryDelay { get; set; } = 1000; + private readonly Lazy _grpc = new(() => new RawGrpcClient(clientOptions)); + + /// + /// The gRPC client used to make requests. + /// + public RawGrpcClient Grpc => _grpc.Value; + /// /// The client options applied on every request. /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawGrpcClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawGrpcClient.cs new file mode 100644 index 000000000000..2326d6b36f8c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/RawGrpcClient.cs @@ -0,0 +1,61 @@ +using Grpc.Core; +using Grpc.Net.Client; + +namespace SeedApi.Core; + +/// +/// Utility class for making gRPC requests to the API. +/// +internal class RawGrpcClient +{ + /// + /// The gRPC channel used to make requests. + /// + public readonly GrpcChannel Channel; + + private readonly ClientOptions _clientOptions; + + public RawGrpcClient(ClientOptions clientOptions) + { + _clientOptions = clientOptions; + + var grpcOptions = PrepareGrpcChannelOptions(); + Channel = grpcOptions is not null + ? GrpcChannel.ForAddress(_clientOptions.BaseUrl, grpcOptions) + : GrpcChannel.ForAddress(_clientOptions.BaseUrl); + } + + /// + /// Creates CallOptions for a gRPC request with the provided metadata, timeout, and credentials. + /// Metadata (headers) should be built at the endpoint level before calling this method. + /// + public CallOptions CreateCallOptions( + global::Grpc.Core.Metadata metadata, + GrpcRequestOptions options, + CancellationToken cancellationToken = default + ) + { + var timeout = options.Timeout ?? _clientOptions.Timeout; + var deadline = DateTime.UtcNow.Add(timeout); + return new CallOptions( + metadata, + deadline, + cancellationToken, + options.WriteOptions, + null, + options.CallCredentials + ); + } + + private GrpcChannelOptions? PrepareGrpcChannelOptions() + { + var grpcChannelOptions = _clientOptions.GrpcOptions; + if (grpcChannelOptions is null) + { + return null; + } + grpcChannelOptions.HttpClient ??= _clientOptions.HttpClient; + grpcChannelOptions.MaxRetryAttempts ??= _clientOptions.MaxRetries; + return grpcChannelOptions; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/DataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/DataserviceClient.cs index 984e4e741a6b..d7a55705956f 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/DataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/DataserviceClient.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using Data.V1.Grpc; +using Grpc.Core; using SeedApi.Core; namespace SeedApi; @@ -7,9 +9,15 @@ public partial class DataserviceClient : IDataserviceClient { private readonly RawClient _client; + private readonly RawGrpcClient _grpc; + + private DataService.DataServiceClient _dataService; + internal DataserviceClient(RawClient client) { _client = client; + _grpc = _client.Grpc; + _dataService = new DataService.DataServiceClient(_grpc.Channel); } private async Task>> FooAsyncCore( @@ -90,4 +98,422 @@ internal DataserviceClient(RawClient client) FooAsyncCore(options, cancellationToken) ); } + + /// + /// await client.Dataservice.UploadAsync( + /// new UploadRequest + /// { + /// Columns = new List<SeedApi.Column>() + /// { + /// new SeedApi.Column + /// { + /// Id = "id", + /// Values = new List<float>() { 1.1f }, + /// }, + /// }, + /// } + /// ); + /// + public async Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UploadAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UploadResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.DeleteAsync(new DeleteRequest()); + /// + public async Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DeleteAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DeleteResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.DescribeAsync(new DescribeRequest()); + /// + public async Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DescribeAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DescribeResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.FetchAsync(new FetchRequest()); + /// + public async Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.FetchAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return FetchResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.ListAsync(new ListRequest()); + /// + public async Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.ListAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return ListResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); + /// + public async Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.QueryAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return QueryResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); + /// + public async Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UpdateAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UpdateResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/IDataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/IDataserviceClient.cs index 790e52ff98cd..4a7de66ccb27 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/IDataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/IDataserviceClient.cs @@ -6,4 +6,46 @@ public partial interface IDataserviceClient RequestOptions? options = null, CancellationToken cancellationToken = default ); + + Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DeleteRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DeleteRequest.cs new file mode 100644 index 000000000000..6226129bb2dc --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DeleteRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteRequest +{ + [JsonPropertyName("ids")] + public IEnumerable? Ids { get; set; } + + [JsonPropertyName("delete_all")] + public bool? DeleteAll { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + /// + /// Maps the DeleteRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DeleteRequest ToProto() + { + var result = new Proto.DeleteRequest(); + if (Ids != null && Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (DeleteAll != null) + { + result.DeleteAll = DeleteAll ?? false; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DescribeRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DescribeRequest.cs new file mode 100644 index 000000000000..fcc72788bd2c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/DescribeRequest.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record DescribeRequest +{ + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("after")] + public DateTime? After { get; set; } + + /// + /// Maps the DescribeRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DescribeRequest ToProto() + { + var result = new Proto.DescribeRequest(); + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (After != null) + { + result.After = WellKnownProto.Timestamp.FromDateTime(After.Value.ToUniversalTime()); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/FetchRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/FetchRequest.cs new file mode 100644 index 000000000000..3ca54d9f8be1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/FetchRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchRequest +{ + [JsonIgnore] + public IEnumerable Ids { get; set; } = new List(); + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the FetchRequest type into its Protobuf-equivalent representation. + /// + internal Proto.FetchRequest ToProto() + { + var result = new Proto.FetchRequest(); + if (Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/ListRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/ListRequest.cs new file mode 100644 index 000000000000..fd26665984e4 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/ListRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListRequest +{ + [JsonIgnore] + public string? Prefix { get; set; } + + [JsonIgnore] + public uint? Limit { get; set; } + + [JsonIgnore] + public string? PaginationToken { get; set; } + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the ListRequest type into its Protobuf-equivalent representation. + /// + internal Proto.ListRequest ToProto() + { + var result = new Proto.ListRequest(); + if (Prefix != null) + { + result.Prefix = Prefix ?? ""; + } + if (Limit != null) + { + result.Limit = Limit ?? 0; + } + if (PaginationToken != null) + { + result.PaginationToken = PaginationToken ?? ""; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/QueryRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/QueryRequest.cs new file mode 100644 index 000000000000..0586a3c144b2 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/QueryRequest.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryRequest +{ + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("top_k")] + public required uint TopK { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("include_values")] + public bool? IncludeValues { get; set; } + + [JsonPropertyName("include_metadata")] + public bool? IncludeMetadata { get; set; } + + [JsonPropertyName("queries")] + public IEnumerable? Queries { get; set; } + + [JsonPropertyName("column")] + public IEnumerable? Column { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + /// + /// Maps the QueryRequest type into its Protobuf-equivalent representation. + /// + internal Proto.QueryRequest ToProto() + { + var result = new Proto.QueryRequest(); + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + result.TopK = TopK; + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IncludeValues != null) + { + result.IncludeValues = IncludeValues ?? false; + } + if (IncludeMetadata != null) + { + result.IncludeMetadata = IncludeMetadata ?? false; + } + if (Queries != null && Queries.Any()) + { + result.Queries.AddRange(Queries.Select(elem => elem.ToProto())); + } + if (Column != null && Column.Any()) + { + result.Column.AddRange(Column); + } + if (Id != null) + { + result.Id = Id ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UpdateRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UpdateRequest.cs new file mode 100644 index 000000000000..40238253ea5e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UpdateRequest.cs @@ -0,0 +1,100 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UpdateRequest +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonPropertyName("set_metadata")] + public Metadata? SetMetadata { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + /// + /// Maps the UpdateRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UpdateRequest ToProto() + { + var result = new Proto.UpdateRequest(); + result.Id = Id; + if (Values != null && Values.Any()) + { + result.Values.AddRange(Values); + } + if (SetMetadata != null) + { + result.SetMetadata = SetMetadata.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UploadRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UploadRequest.cs new file mode 100644 index 000000000000..b8b1d3749c34 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Requests/UploadRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadRequest +{ + [JsonPropertyName("columns")] + public IEnumerable Columns { get; set; } = new List(); + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + /// + /// Maps the UploadRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UploadRequest ToProto() + { + var result = new Proto.UploadRequest(); + if (Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs new file mode 100644 index 000000000000..efdca475318f --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs @@ -0,0 +1,67 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct IndexType : IStringEnum +{ + public static readonly IndexType IndexTypeInvalid = new(Values.IndexTypeInvalid); + + public static readonly IndexType IndexTypeDefault = new(Values.IndexTypeDefault); + + public static readonly IndexType IndexTypeStrict = new(Values.IndexTypeStrict); + + public IndexType(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static IndexType FromCustom(string value) + { + return new IndexType(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(IndexType value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(IndexType value1, string value2) => !value1.Value.Equals(value2); + + public static explicit operator string(IndexType value) => value.Value; + + public static explicit operator IndexType(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string IndexTypeInvalid = "INDEX_TYPE_INVALID"; + + public const string IndexTypeDefault = "INDEX_TYPE_DEFAULT"; + + public const string IndexTypeStrict = "INDEX_TYPE_STRICT"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApi.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApi.csproj index 00ee3f51f8c4..9f74bcc70d1a 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApi.csproj +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/SeedApi.csproj @@ -45,6 +45,39 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Column.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Column.cs new file mode 100644 index 000000000000..ef225e7518ab --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Column.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Column : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Column type from its Protobuf-equivalent representation. + /// + internal static Column FromProto(ProtoDataV1Grpc.Column value) + { + return new Column + { + Id = value.Id, + Values = value.Values?.ToList() ?? Enumerable.Empty(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Column type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Column ToProto() + { + var result = new ProtoDataV1Grpc.Column(); + result.Id = Id; + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DeleteResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DeleteResponse.cs new file mode 100644 index 000000000000..2c72cc2beea7 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DeleteResponse.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DeleteResponse type from its Protobuf-equivalent representation. + /// + internal static DeleteResponse FromProto(ProtoDataV1Grpc.DeleteResponse value) + { + return new DeleteResponse(); + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DeleteResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DeleteResponse ToProto() + { + return new ProtoDataV1Grpc.DeleteResponse(); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DescribeResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DescribeResponse.cs new file mode 100644 index 000000000000..be7b2a6f4b01 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/DescribeResponse.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DescribeResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("namespaces")] + public Dictionary? Namespaces { get; set; } + + [JsonPropertyName("dimension")] + public uint? Dimension { get; set; } + + [JsonPropertyName("fullness")] + public float? Fullness { get; set; } + + [JsonPropertyName("total_count")] + public uint? TotalCount { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DescribeResponse type from its Protobuf-equivalent representation. + /// + internal static DescribeResponse FromProto(ProtoDataV1Grpc.DescribeResponse value) + { + return new DescribeResponse + { + Namespaces = value.Namespaces?.ToDictionary( + kvp => kvp.Key, + kvp => NamespaceSummary.FromProto(kvp.Value) + ), + Dimension = value.Dimension, + Fullness = value.Fullness, + TotalCount = value.TotalCount, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DescribeResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DescribeResponse ToProto() + { + var result = new ProtoDataV1Grpc.DescribeResponse(); + if (Namespaces != null && Namespaces.Any()) + { + foreach (var kvp in Namespaces) + { + result.Namespaces.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Dimension != null) + { + result.Dimension = Dimension ?? 0; + } + if (Fullness != null) + { + result.Fullness = Fullness ?? 0.0f; + } + if (TotalCount != null) + { + result.TotalCount = TotalCount ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FetchResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FetchResponse.cs new file mode 100644 index 000000000000..0558b91da66e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FetchResponse.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public Dictionary? Columns { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new FetchResponse type from its Protobuf-equivalent representation. + /// + internal static FetchResponse FromProto(ProtoDataV1Grpc.FetchResponse value) + { + return new FetchResponse + { + Columns = value.Columns?.ToDictionary( + kvp => kvp.Key, + kvp => Column.FromProto(kvp.Value) + ), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the FetchResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.FetchResponse ToProto() + { + var result = new ProtoDataV1Grpc.FetchResponse(); + if (Columns != null && Columns.Any()) + { + foreach (var kvp in Columns) + { + result.Columns.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs new file mode 100644 index 000000000000..38cc250adbcf --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs @@ -0,0 +1,95 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct FieldBehavior : IStringEnum +{ + public static readonly FieldBehavior FieldBehaviorUnspecified = new( + Values.FieldBehaviorUnspecified + ); + + public static readonly FieldBehavior Optional = new(Values.Optional); + + public static readonly FieldBehavior Required = new(Values.Required); + + public static readonly FieldBehavior OutputOnly = new(Values.OutputOnly); + + public static readonly FieldBehavior InputOnly = new(Values.InputOnly); + + public static readonly FieldBehavior Immutable = new(Values.Immutable); + + public static readonly FieldBehavior UnorderedList = new(Values.UnorderedList); + + public static readonly FieldBehavior NonEmptyDefault = new(Values.NonEmptyDefault); + + public static readonly FieldBehavior Identifier = new(Values.Identifier); + + public FieldBehavior(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static FieldBehavior FromCustom(string value) + { + return new FieldBehavior(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(FieldBehavior value1, string value2) => + value1.Value.Equals(value2); + + public static bool operator !=(FieldBehavior value1, string value2) => + !value1.Value.Equals(value2); + + public static explicit operator string(FieldBehavior value) => value.Value; + + public static explicit operator FieldBehavior(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string FieldBehaviorUnspecified = "FIELD_BEHAVIOR_UNSPECIFIED"; + + public const string Optional = "OPTIONAL"; + + public const string Required = "REQUIRED"; + + public const string OutputOnly = "OUTPUT_ONLY"; + + public const string InputOnly = "INPUT_ONLY"; + + public const string Immutable = "IMMUTABLE"; + + public const string UnorderedList = "UNORDERED_LIST"; + + public const string NonEmptyDefault = "NON_EMPTY_DEFAULT"; + + public const string Identifier = "IDENTIFIER"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/IndexedData.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/IndexedData.cs new file mode 100644 index 000000000000..4cf44e0ced78 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/IndexedData.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record IndexedData : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("indices")] + public IEnumerable Indices { get; set; } = new List(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new IndexedData type from its Protobuf-equivalent representation. + /// + internal static IndexedData FromProto(ProtoDataV1Grpc.IndexedData value) + { + return new IndexedData + { + Indices = value.Indices?.ToList() ?? Enumerable.Empty(), + Values = value.Values?.ToList() ?? Enumerable.Empty(), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the IndexedData type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.IndexedData ToProto() + { + var result = new ProtoDataV1Grpc.IndexedData(); + if (Indices.Any()) + { + result.Indices.AddRange(Indices); + } + if (Values.Any()) + { + result.Values.AddRange(Values); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListElement.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListElement.cs new file mode 100644 index 000000000000..ff0e19b9e9a9 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListElement.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListElement : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListElement type from its Protobuf-equivalent representation. + /// + internal static ListElement FromProto(ProtoDataV1Grpc.ListElement value) + { + return new ListElement { Id = value.Id }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListElement type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListElement ToProto() + { + var result = new ProtoDataV1Grpc.ListElement(); + if (Id != null) + { + result.Id = Id ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListResponse.cs new file mode 100644 index 000000000000..72b63f8287fd --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ListResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public IEnumerable? Columns { get; set; } + + [JsonPropertyName("pagination")] + public Pagination? Pagination { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListResponse type from its Protobuf-equivalent representation. + /// + internal static ListResponse FromProto(ProtoDataV1Grpc.ListResponse value) + { + return new ListResponse + { + Columns = value.Columns?.Select(ListElement.FromProto), + Pagination = value.Pagination != null ? Pagination.FromProto(value.Pagination) : null, + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListResponse ToProto() + { + var result = new ProtoDataV1Grpc.ListResponse(); + if (Columns != null && Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Pagination != null) + { + result.Pagination = Pagination.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Metadata.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Metadata.cs new file mode 100644 index 000000000000..a767a7f0bdd6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Metadata.cs @@ -0,0 +1,39 @@ +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } + + internal static Metadata FromProto(WellKnownProto.Struct value) + { + var result = new Metadata(); + foreach (var kvp in value.Fields) + { + result[kvp.Key] = kvp.Value != null ? MetadataValue.FromProto(kvp.Value) : null; + } + return result; + } + + internal WellKnownProto.Struct ToProto() + { + var result = new WellKnownProto.Struct(); + foreach (var kvp in this) + { + result.Fields[kvp.Key] = kvp.Value?.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/MetadataValue.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/MetadataValue.cs new file mode 100644 index 000000000000..51b7b46595b3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/MetadataValue.cs @@ -0,0 +1,91 @@ +using OneOf; +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class MetadataValue( + OneOf, Metadata> value +) : OneOfBase, Metadata>(value) +{ + internal static MetadataValue? FromProto(WellKnownProto.Value value) + { + return value.KindCase switch + { + WellKnownProto.Value.KindOneofCase.StringValue => value.StringValue, + WellKnownProto.Value.KindOneofCase.NumberValue => value.NumberValue, + WellKnownProto.Value.KindOneofCase.BoolValue => value.BoolValue, + WellKnownProto.Value.KindOneofCase.ListValue => value + .ListValue.Values.Select(FromProto) + .ToList(), + WellKnownProto.Value.KindOneofCase.StructValue => Metadata.FromProto(value.StructValue), + _ => null, + }; + } + + internal WellKnownProto.Value ToProto() + { + return Match( + WellKnownProto.Value.ForString, + WellKnownProto.Value.ForNumber, + WellKnownProto.Value.ForBool, + list => new WellKnownProto.Value + { + ListValue = new WellKnownProto.ListValue + { + Values = { list.Select(item => item?.ToProto()) }, + }, + }, + nested => new WellKnownProto.Value { StructValue = nested.ToProto() } + ); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } + + public static implicit operator MetadataValue(string value) => new(value); + + public static implicit operator MetadataValue(bool value) => new(value); + + public static implicit operator MetadataValue(double value) => new(value); + + public static implicit operator MetadataValue(Metadata value) => new(value); + + public static implicit operator MetadataValue(MetadataValue?[] value) => new(value); + + public static implicit operator MetadataValue(List value) => new(value); + + public static implicit operator MetadataValue(string[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(bool[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(bool?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/NamespaceSummary.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/NamespaceSummary.cs new file mode 100644 index 000000000000..b302c01763b5 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/NamespaceSummary.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record NamespaceSummary : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new NamespaceSummary type from its Protobuf-equivalent representation. + /// + internal static NamespaceSummary FromProto(ProtoDataV1Grpc.NamespaceSummary value) + { + return new NamespaceSummary { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the NamespaceSummary type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.NamespaceSummary ToProto() + { + var result = new ProtoDataV1Grpc.NamespaceSummary(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Pagination.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Pagination.cs new file mode 100644 index 000000000000..2eb1d1eeedec --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Pagination.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Pagination : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("next")] + public string? Next { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Pagination type from its Protobuf-equivalent representation. + /// + internal static Pagination FromProto(ProtoDataV1Grpc.Pagination value) + { + return new Pagination { Next = value.Next }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Pagination type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Pagination ToProto() + { + var result = new ProtoDataV1Grpc.Pagination(); + if (Next != null) + { + result.Next = Next ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryColumn.cs new file mode 100644 index 000000000000..d2621a6d87c6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryColumn.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("values")] + public IEnumerable Values { get; set; } = new List(); + + [JsonPropertyName("top_k")] + public uint? TopK { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryColumn type from its Protobuf-equivalent representation. + /// + internal static QueryColumn FromProto(ProtoDataV1Grpc.QueryColumn value) + { + return new QueryColumn + { + Values = value.Values?.ToList() ?? Enumerable.Empty(), + TopK = value.TopK, + Namespace = value.Namespace, + Filter = value.Filter != null ? Metadata.FromProto(value.Filter) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryColumn ToProto() + { + var result = new ProtoDataV1Grpc.QueryColumn(); + if (Values.Any()) + { + result.Values.AddRange(Values); + } + if (TopK != null) + { + result.TopK = TopK ?? 0; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResponse.cs new file mode 100644 index 000000000000..9242ccd094a1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("results")] + public IEnumerable? Results { get; set; } + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResponse type from its Protobuf-equivalent representation. + /// + internal static QueryResponse FromProto(ProtoDataV1Grpc.QueryResponse value) + { + return new QueryResponse + { + Results = value.Results?.Select(QueryResult.FromProto), + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResponse ToProto() + { + var result = new ProtoDataV1Grpc.QueryResponse(); + if (Results != null && Results.Any()) + { + result.Results.AddRange(Results.Select(elem => elem.ToProto())); + } + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResult.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResult.cs new file mode 100644 index 000000000000..8a0ab47d124a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/QueryResult.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResult : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResult type from its Protobuf-equivalent representation. + /// + internal static QueryResult FromProto(ProtoDataV1Grpc.QueryResult value) + { + return new QueryResult + { + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResult type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResult ToProto() + { + var result = new ProtoDataV1Grpc.QueryResult(); + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ScoredColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ScoredColumn.cs new file mode 100644 index 000000000000..a0d7fc819768 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/ScoredColumn.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ScoredColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("score")] + public float? Score { get; set; } + + [JsonPropertyName("values")] + public IEnumerable? Values { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ScoredColumn type from its Protobuf-equivalent representation. + /// + internal static ScoredColumn FromProto(ProtoDataV1Grpc.ScoredColumn value) + { + return new ScoredColumn + { + Id = value.Id, + Score = value.Score, + Values = value.Values?.ToList(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ScoredColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ScoredColumn ToProto() + { + var result = new ProtoDataV1Grpc.ScoredColumn(); + result.Id = Id; + if (Score != null) + { + result.Score = Score ?? 0.0f; + } + if (Values != null && Values.Any()) + { + result.Values.AddRange(Values); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UpdateResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UpdateResponse.cs new file mode 100644 index 000000000000..643f8a79e161 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UpdateResponse.cs @@ -0,0 +1,116 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record UpdateResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("updated_at")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UpdateResponse type from its Protobuf-equivalent representation. + /// + internal static UpdateResponse FromProto(ProtoDataV1Grpc.UpdateResponse value) + { + return new UpdateResponse + { + UpdatedAt = value.UpdatedAt.ToDateTime(), + IndexType = value.IndexType switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexType}"), + }, + Details = value.Details != null ? value.Details : null, + IndexTypes = value.IndexTypes.Select(type => + type switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexTypes}"), + } + ), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UpdateResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UpdateResponse ToProto() + { + var result = new ProtoDataV1Grpc.UpdateResponse(); + if (UpdatedAt != null) + { + result.UpdatedAt = WellKnownProto.Timestamp.FromDateTime( + UpdatedAt.Value.ToUniversalTime() + ); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UploadResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UploadResponse.cs new file mode 100644 index 000000000000..2409ef7d7bff --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/UploadResponse.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UploadResponse type from its Protobuf-equivalent representation. + /// + internal static UploadResponse FromProto(ProtoDataV1Grpc.UploadResponse value) + { + return new UploadResponse { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UploadResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UploadResponse ToProto() + { + var result = new ProtoDataV1Grpc.UploadResponse(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Usage.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Usage.cs new file mode 100644 index 000000000000..c6c1ccf37d4a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/Usage.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Usage : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("units")] + public uint? Units { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Usage type from its Protobuf-equivalent representation. + /// + internal static Usage FromProto(ProtoDataV1Grpc.Usage value) + { + return new Usage { Units = value.Units }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Usage type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Usage ToProto() + { + var result = new ProtoDataV1Grpc.Usage(); + if (Units != null) + { + result.Units = Units ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/README.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/README.md index 79564745a6ec..7cc184191f18 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/README.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/README.md @@ -15,9 +15,8 @@ The Seed C# library provides convenient access to the Seed APIs from C#. - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) - - [Raw Response](#raw-response) - [Additional Headers](#additional-headers) - - [Additional Query Parameters](#additional-query-parameters) + - [Forward Compatible Enums](#forward-compatible-enums) - [Contributing](#contributing) ## Requirements @@ -99,34 +98,6 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Raw Response - -Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. - -```csharp -using SeedApi; - -// Access raw response data (status code, headers, etc.) alongside the parsed response -var result = await client.Dataservice.FooAsync(...).WithRawResponse(); - -// Access the parsed data -var data = result.Data; - -// Access raw response metadata -var statusCode = result.RawResponse.StatusCode; -var headers = result.RawResponse.Headers; -var url = result.RawResponse.Url; - -// Access specific headers (case-insensitive) -if (headers.TryGetValue("X-Request-Id", out var requestId)) -{ - System.Console.WriteLine($"Request ID: {requestId}"); -} - -// For the default behavior, simply await without .WithRawResponse() -var data = await client.Dataservice.FooAsync(...); -``` - ### Additional Headers If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. @@ -143,20 +114,33 @@ var response = await client.Dataservice.FooAsync( ); ``` -### Additional Query Parameters +### Forward Compatible Enums -If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. +This SDK uses forward-compatible enums that can handle unknown values gracefully. ```csharp -var response = await client.Dataservice.FooAsync( - ..., - new RequestOptions { - AdditionalQueryParameters = new Dictionary - { - { "custom_param", "custom-value" } - } - } -); +using SeedApi; + +// Using a built-in value +var indexType = IndexType.IndexTypeInvalid; + +// Using a custom value +var customIndexType = IndexType.FromCustom("custom-value"); + +// Using in a switch statement +switch (indexType.Value) +{ + case IndexType.Values.IndexTypeInvalid: + Console.WriteLine("IndexTypeInvalid"); + break; + default: + Console.WriteLine($"Unknown value: {indexType.Value}"); + break; +} + +// Explicit casting +string indexTypeString = (string)IndexType.IndexTypeInvalid; +IndexType indexTypeFromString = (IndexType)"INDEX_TYPE_INVALID"; ``` ## Contributing diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/data/v1/data.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/data/v1/data.proto new file mode 100644 index 000000000000..b8244b95516d --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/data/v1/data.proto @@ -0,0 +1,230 @@ +syntax = "proto3"; + +package data.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; + +option csharp_namespace = "Data.V1.Grpc"; +option go_package = "github.com/acme.co/data-go-grpc"; + +enum IndexType { + INDEX_TYPE_INVALID = 0; + INDEX_TYPE_DEFAULT = 1; + INDEX_TYPE_STRICT = 2; +} + +message IndexedData { + repeated uint32 indices = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; +} + +message Column { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct metadata = 3; + IndexedData indexed_data = 4; +} + +message ScoredColumn { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + float score = 2; + repeated float values = 3; + google.protobuf.Struct metadata = 4; + IndexedData indexed_data = 5; +} + +message UploadRequest { + repeated Column columns = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message UploadResponse { + uint32 count = 1; +} + +message DeleteRequest { + repeated string ids = 1; + bool delete_all = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; +} + +message DeleteResponse {} + +message FetchRequest { + repeated string ids = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + string namespace = 2; +} + +message FetchResponse { + map columns = 1; + string namespace = 2; + optional Usage usage = 3; +} + +message ListRequest { + optional string prefix = 1; + optional uint32 limit = 2; + optional string pagination_token = 3; + string namespace = 4; +} + +message Pagination { + string next = 1; +} + +message ListElement { + string id = 1; +} + +message ListResponse { + repeated ListElement columns = 1; + optional Pagination pagination = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message QueryColumn { + repeated float values = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + uint32 top_k = 2; + string namespace = 3; + google.protobuf.Struct filter = 4; + IndexedData indexed_data = 5; +} + +message QueryRequest { + string namespace = 1; + uint32 top_k = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + google.protobuf.Struct filter = 3; + bool include_values = 4; + bool include_metadata = 5; + repeated QueryColumn queries = 6 [ + deprecated = true + ]; + repeated float column = 7; + string id = 8; + IndexedData indexed_data = 9; +} + +message QueryResult { + repeated ScoredColumn matches = 1; + string namespace = 2; +} + +message QueryResponse { + repeated QueryResult results = 1 [deprecated=true]; + repeated ScoredColumn matches = 2; + string namespace = 3; + optional Usage usage = 4; +} + +message Usage { + optional uint32 units = 1; +} + +message UpdateRequest { + string id = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + repeated float values = 2; + google.protobuf.Struct set_metadata = 3; + string namespace = 4; + IndexedData indexed_data = 5; + IndexType index_type = 6; + google.protobuf.Any details = 7; + repeated IndexType index_types = 8; +} + +message UpdateResponse { + google.protobuf.Timestamp updated_at = 1; + IndexType index_type = 2; + google.protobuf.Any details = 3; + repeated IndexType index_types = 4; +} + +message DescribeRequest { + google.protobuf.Struct filter = 1; + google.protobuf.Timestamp after = 2; +} + +message NamespaceSummary { + uint32 count = 1; +} + +message DescribeResponse { + map namespaces = 1; + uint32 dimension = 2; + float fullness = 3; + uint32 total_count = 4; +} + +service DataService { + rpc Upload(UploadRequest) returns (UploadResponse) { + option (google.api.http) = { + post: "/data" + body: "*" + }; + } + + rpc Delete(DeleteRequest) returns (DeleteResponse) { + option (google.api.http) = { + post: "/data/delete" + body: "*" + }; + } + + rpc Fetch(FetchRequest) returns (FetchResponse) { + option (google.api.http) = { + get: "/data/fetch" + }; + } + + rpc List(ListRequest) returns (ListResponse) { + option (google.api.http) = { + get: "/data/list" + }; + } + + rpc Query(QueryRequest) returns (QueryResponse) { + option (google.api.http) = { + post: "/data/query" + body: "*" + }; + } + + rpc Update(UpdateRequest) returns (UpdateResponse) { + option (google.api.http) = { + post: "/data/update" + body: "*" + }; + } + + rpc Describe(DescribeRequest) returns (DescribeResponse) { + option (google.api.http) = { + post: "/data/describe" + body: "*" + }; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/annotations.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/annotations.proto new file mode 100644 index 000000000000..8ff42098404c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/field_behavior.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/field_behavior.proto new file mode 100644 index 000000000000..128799c558db --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/http.proto b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/http.proto new file mode 100644 index 000000000000..c8392381eb99 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/proto/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/reference.md b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/reference.md index bfc647a5cfb6..2a99e57fae10 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/reference.md +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/reference.md @@ -1,5 +1,5 @@ # Reference -## Dataservice +## DataService
client.Dataservice.FooAsync() -> WithRawResponseTask<Dictionary<string, object?>>
@@ -25,3 +25,291 @@ await client.Dataservice.FooAsync();
+
client.Dataservice.UploadAsync(UploadRequest { ... }) -> WithRawResponseTask<UploadResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UploadAsync( + new UploadRequest + { + Columns = new List() + { + new SeedApi.Column { Id = "id", Values = new[] { 1.1f } }, + }, + } +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UploadRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DeleteAsync(DeleteRequest { ... }) -> WithRawResponseTask<DeleteResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DeleteAsync(new DeleteRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DeleteRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.DescribeAsync(DescribeRequest { ... }) -> WithRawResponseTask<DescribeResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.DescribeAsync(new DescribeRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `DescribeRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.FetchAsync(FetchRequest { ... }) -> WithRawResponseTask<FetchResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.FetchAsync(new FetchRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `FetchRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.ListAsync(ListRequest { ... }) -> WithRawResponseTask<ListResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.ListAsync(new ListRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ListRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.QueryAsync(QueryRequest { ... }) -> WithRawResponseTask<QueryResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `QueryRequest` + +
+
+
+
+ + +
+
+
+ +
client.Dataservice.UpdateAsync(UpdateRequest { ... }) -> WithRawResponseTask<UpdateResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `UpdateRequest` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/snippet.json b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/snippet.json index 6cef6bd0ec54..6f1de7660adc 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/snippet.json +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/snippet.json @@ -12,6 +12,90 @@ "type": "csharp", "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FooAsync();\n" } + }, + { + "example_identifier": null, + "id": { + "path": "/data", + "method": "POST", + "identifier_override": "endpoint_dataservice.upload" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UploadAsync(\n new UploadRequest\n {\n Columns = new List()\n {\n new SeedApi.Column { Id = \"id\", Values = new[] { 1.1f } },\n },\n }\n);\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/delete", + "method": "POST", + "identifier_override": "endpoint_dataservice.delete" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DeleteAsync(new DeleteRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/describe", + "method": "POST", + "identifier_override": "endpoint_dataservice.describe" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.DescribeAsync(new DescribeRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/fetch", + "method": "GET", + "identifier_override": "endpoint_dataservice.fetch" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.FetchAsync(new FetchRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/list", + "method": "GET", + "identifier_override": "endpoint_dataservice.list" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.ListAsync(new ListRequest());\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/query", + "method": "POST", + "identifier_override": "endpoint_dataservice.query" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 });\n" + } + }, + { + "example_identifier": null, + "id": { + "path": "/data/update", + "method": "POST", + "identifier_override": "endpoint_dataservice.update" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Dataservice.UpdateAsync(new UpdateRequest { Id = \"id\" });\n" + } } ] } \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example0.cs deleted file mode 100644 index 5c845cf2e096..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example0.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example0 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example1.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example1.cs deleted file mode 100644 index dc9e849f6c96..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/Example1.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SeedApi; - -namespace Usage; - -public class Example1 -{ - public async Task Do() { - var client = new SeedApiClient( - clientOptions: new ClientOptions { - BaseUrl = "https://api.fern.com" - } - ); - - await client.Dataservice.FooAsync(); - } - -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj deleted file mode 100644 index 3417db2e58e2..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - 12 - enable - enable - - - - - - \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs deleted file mode 100644 index cb111a797b9a..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/BaseMockServerTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using NUnit.Framework; -using SeedApi; -using WireMock.Logging; -using WireMock.Server; -using WireMock.Settings; - -namespace SeedApi.Test.Unit.MockServer; - -[SetUpFixture] -public class BaseMockServerTest -{ - protected static WireMockServer Server { get; set; } = null!; - - protected static SeedApiClient Client { get; set; } = null!; - - protected static RequestOptions RequestOptions { get; set; } = new(); - - [OneTimeSetUp] - public void GlobalSetup() - { - // Start the WireMock server - Server = WireMockServer.Start( - new WireMockServerSettings { Logger = new WireMockConsoleLogger() } - ); - - // Initialize the Client - Client = new SeedApiClient( - clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } - ); - } - - [OneTimeTearDown] - public void GlobalTeardown() - { - Server.Stop(); - Server.Dispose(); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs deleted file mode 100644 index 3a6c904371a3..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Unit/MockServer/Dataservice/FooTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using NUnit.Framework; -using SeedApi.Test.Unit.MockServer; -using SeedApi.Test.Utils; - -namespace SeedApi.Test.Unit.MockServer.Dataservice; - -[TestFixture] -public class FooTest : BaseMockServerTest -{ - [NUnit.Framework.Test] - public async Task MockServerTest_1() - { - const string mockResponse = """ - { - "string": { - "key": "value" - } - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } - - [NUnit.Framework.Test] - public async Task MockServerTest_2() - { - const string mockResponse = """ - { - "key": "value" - } - """; - - Server - .Given(WireMock.RequestBuilders.Request.Create().WithPath("/foo").UsingPost()) - .RespondWith( - WireMock - .ResponseBuilders.Response.Create() - .WithStatusCode(200) - .WithBody(mockResponse) - ); - - var response = await Client.Dataservice.FooAsync(); - JsonAssert.AreEqual(response, mockResponse); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/ProtoAnyMapper.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/ProtoAnyMapper.cs new file mode 100644 index 000000000000..5c55aa625072 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/ProtoAnyMapper.cs @@ -0,0 +1,28 @@ +using global::System.Reflection; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi.Core; + +public static class ProtoAnyMapper +{ + public static Any? ToProto(object? value) + { + if (value is null) + { + return null; + } + var toProtoMethod = value + .GetType() + .GetMethod("ToProto", BindingFlags.Instance | BindingFlags.NonPublic); + if (toProtoMethod is null) + { + throw new InvalidOperationException( + $"Type {value.GetType()} does not have a ToProto method" + ); + } + var protoValue = toProtoMethod.Invoke(value, null); + return WellKnownProto.Any.Pack((IMessage)protoValue); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/ClientOptions.cs index 837716a987f2..6cf621e7bb97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/ClientOptions.cs @@ -1,3 +1,4 @@ +using Grpc.Net.Client; using SeedApi.Core; namespace SeedApi; @@ -66,6 +67,17 @@ public partial class ClientOptions #endif } = TimeSpan.FromSeconds(30); + /// + /// The options used for gRPC client endpoints. + /// + public GrpcChannelOptions? GrpcOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + /// /// Clones this and returns a new instance /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/GrpcRequestOptions.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/GrpcRequestOptions.cs new file mode 100644 index 000000000000..4fb311172634 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/Public/GrpcRequestOptions.cs @@ -0,0 +1,63 @@ +using Grpc.Core; +using SeedApi.Core; + +namespace SeedApi; + +public partial class GrpcRequestOptions +{ + /// + /// The maximum number of retry attempts. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Options for write operations. + /// + public WriteOptions? WriteOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Client-side call credentials. Provide authorization with per-call granularity. + /// + public CallCredentials? CallCredentials { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with this particular request. + /// Headers with matching keys will be overwritten by headers set on the client options. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new List>(); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawClient.cs index d42791fcc0e0..3e191c04dbe4 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawClient.cs @@ -20,6 +20,13 @@ internal partial class RawClient(ClientOptions clientOptions) #endif internal int BaseRetryDelay { get; set; } = 1000; + private readonly Lazy _grpc = new(() => new RawGrpcClient(clientOptions)); + + /// + /// The gRPC client used to make requests. + /// + public RawGrpcClient Grpc => _grpc.Value; + /// /// The client options applied on every request. /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawGrpcClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawGrpcClient.cs new file mode 100644 index 000000000000..2326d6b36f8c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/RawGrpcClient.cs @@ -0,0 +1,61 @@ +using Grpc.Core; +using Grpc.Net.Client; + +namespace SeedApi.Core; + +/// +/// Utility class for making gRPC requests to the API. +/// +internal class RawGrpcClient +{ + /// + /// The gRPC channel used to make requests. + /// + public readonly GrpcChannel Channel; + + private readonly ClientOptions _clientOptions; + + public RawGrpcClient(ClientOptions clientOptions) + { + _clientOptions = clientOptions; + + var grpcOptions = PrepareGrpcChannelOptions(); + Channel = grpcOptions is not null + ? GrpcChannel.ForAddress(_clientOptions.BaseUrl, grpcOptions) + : GrpcChannel.ForAddress(_clientOptions.BaseUrl); + } + + /// + /// Creates CallOptions for a gRPC request with the provided metadata, timeout, and credentials. + /// Metadata (headers) should be built at the endpoint level before calling this method. + /// + public CallOptions CreateCallOptions( + global::Grpc.Core.Metadata metadata, + GrpcRequestOptions options, + CancellationToken cancellationToken = default + ) + { + var timeout = options.Timeout ?? _clientOptions.Timeout; + var deadline = DateTime.UtcNow.Add(timeout); + return new CallOptions( + metadata, + deadline, + cancellationToken, + options.WriteOptions, + null, + options.CallCredentials + ); + } + + private GrpcChannelOptions? PrepareGrpcChannelOptions() + { + var grpcChannelOptions = _clientOptions.GrpcOptions; + if (grpcChannelOptions is null) + { + return null; + } + grpcChannelOptions.HttpClient ??= _clientOptions.HttpClient; + grpcChannelOptions.MaxRetryAttempts ??= _clientOptions.MaxRetries; + return grpcChannelOptions; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/DataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/DataserviceClient.cs index 984e4e741a6b..e991dba27923 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/DataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/DataserviceClient.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using Data.V1.Grpc; +using Grpc.Core; using SeedApi.Core; namespace SeedApi; @@ -7,9 +9,15 @@ public partial class DataserviceClient : IDataserviceClient { private readonly RawClient _client; + private readonly RawGrpcClient _grpc; + + private DataService.DataServiceClient _dataService; + internal DataserviceClient(RawClient client) { _client = client; + _grpc = _client.Grpc; + _dataService = new DataService.DataServiceClient(_grpc.Channel); } private async Task>> FooAsyncCore( @@ -90,4 +98,418 @@ internal DataserviceClient(RawClient client) FooAsyncCore(options, cancellationToken) ); } + + /// + /// await client.Dataservice.UploadAsync( + /// new UploadRequest + /// { + /// Columns = new List<SeedApi.Column>() + /// { + /// new SeedApi.Column { Id = "id", Values = new[] { 1.1f } }, + /// }, + /// } + /// ); + /// + public async Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UploadAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UploadResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.DeleteAsync(new DeleteRequest()); + /// + public async Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DeleteAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DeleteResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.DescribeAsync(new DescribeRequest()); + /// + public async Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.DescribeAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return DescribeResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.FetchAsync(new FetchRequest()); + /// + public async Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.FetchAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return FetchResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.ListAsync(new ListRequest()); + /// + public async Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.ListAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return ListResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.QueryAsync(new QueryRequest { TopK = 1 }); + /// + public async Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.QueryAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return QueryResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } + + /// + /// await client.Dataservice.UpdateAsync(new UpdateRequest { Id = "id" }); + /// + public async Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _dataService.UpdateAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return UpdateResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/IDataserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/IDataserviceClient.cs index 790e52ff98cd..4a7de66ccb27 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/IDataserviceClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/IDataserviceClient.cs @@ -6,4 +6,46 @@ public partial interface IDataserviceClient RequestOptions? options = null, CancellationToken cancellationToken = default ); + + Task UploadAsync( + UploadRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DeleteAsync( + DeleteRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task DescribeAsync( + DescribeRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task FetchAsync( + FetchRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task ListAsync( + ListRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task QueryAsync( + QueryRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); + + Task UpdateAsync( + UpdateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DeleteRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DeleteRequest.cs new file mode 100644 index 000000000000..6226129bb2dc --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DeleteRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteRequest +{ + [JsonPropertyName("ids")] + public IEnumerable? Ids { get; set; } + + [JsonPropertyName("delete_all")] + public bool? DeleteAll { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + /// + /// Maps the DeleteRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DeleteRequest ToProto() + { + var result = new Proto.DeleteRequest(); + if (Ids != null && Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (DeleteAll != null) + { + result.DeleteAll = DeleteAll ?? false; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DescribeRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DescribeRequest.cs new file mode 100644 index 000000000000..fcc72788bd2c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/DescribeRequest.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record DescribeRequest +{ + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("after")] + public DateTime? After { get; set; } + + /// + /// Maps the DescribeRequest type into its Protobuf-equivalent representation. + /// + internal Proto.DescribeRequest ToProto() + { + var result = new Proto.DescribeRequest(); + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (After != null) + { + result.After = WellKnownProto.Timestamp.FromDateTime(After.Value.ToUniversalTime()); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/FetchRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/FetchRequest.cs new file mode 100644 index 000000000000..3ca54d9f8be1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/FetchRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchRequest +{ + [JsonIgnore] + public IEnumerable Ids { get; set; } = new List(); + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the FetchRequest type into its Protobuf-equivalent representation. + /// + internal Proto.FetchRequest ToProto() + { + var result = new Proto.FetchRequest(); + if (Ids.Any()) + { + result.Ids.AddRange(Ids); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/ListRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/ListRequest.cs new file mode 100644 index 000000000000..fd26665984e4 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/ListRequest.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListRequest +{ + [JsonIgnore] + public string? Prefix { get; set; } + + [JsonIgnore] + public uint? Limit { get; set; } + + [JsonIgnore] + public string? PaginationToken { get; set; } + + [JsonIgnore] + public string? Namespace { get; set; } + + /// + /// Maps the ListRequest type into its Protobuf-equivalent representation. + /// + internal Proto.ListRequest ToProto() + { + var result = new Proto.ListRequest(); + if (Prefix != null) + { + result.Prefix = Prefix ?? ""; + } + if (Limit != null) + { + result.Limit = Limit ?? 0; + } + if (PaginationToken != null) + { + result.PaginationToken = PaginationToken ?? ""; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/QueryRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/QueryRequest.cs new file mode 100644 index 000000000000..89773241acc7 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/QueryRequest.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryRequest +{ + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("top_k")] + public required uint TopK { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("include_values")] + public bool? IncludeValues { get; set; } + + [JsonPropertyName("include_metadata")] + public bool? IncludeMetadata { get; set; } + + [JsonPropertyName("queries")] + public IEnumerable? Queries { get; set; } + + [JsonPropertyName("column")] + public ReadOnlyMemory? Column { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + /// + /// Maps the QueryRequest type into its Protobuf-equivalent representation. + /// + internal Proto.QueryRequest ToProto() + { + var result = new Proto.QueryRequest(); + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + result.TopK = TopK; + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IncludeValues != null) + { + result.IncludeValues = IncludeValues ?? false; + } + if (IncludeMetadata != null) + { + result.IncludeMetadata = IncludeMetadata ?? false; + } + if (Queries != null && Queries.Any()) + { + result.Queries.AddRange(Queries.Select(elem => elem.ToProto())); + } + if (Column != null && !Column.Value.IsEmpty) + { + result.Column.AddRange(Column.Value.ToArray()); + } + if (Id != null) + { + result.Id = Id ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UpdateRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UpdateRequest.cs new file mode 100644 index 000000000000..1edaf1132bd9 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UpdateRequest.cs @@ -0,0 +1,100 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UpdateRequest +{ + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public ReadOnlyMemory? Values { get; set; } + + [JsonPropertyName("set_metadata")] + public Metadata? SetMetadata { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + /// + /// Maps the UpdateRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UpdateRequest ToProto() + { + var result = new Proto.UpdateRequest(); + result.Id = Id; + if (Values != null && !Values.Value.IsEmpty) + { + result.Values.AddRange(Values.Value.ToArray()); + } + if (SetMetadata != null) + { + result.SetMetadata = SetMetadata.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UploadRequest.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UploadRequest.cs new file mode 100644 index 000000000000..b8b1d3749c34 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Requests/UploadRequest.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadRequest +{ + [JsonPropertyName("columns")] + public IEnumerable Columns { get; set; } = new List(); + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + /// + /// Maps the UploadRequest type into its Protobuf-equivalent representation. + /// + internal Proto.UploadRequest ToProto() + { + var result = new Proto.UploadRequest(); + if (Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs new file mode 100644 index 000000000000..efdca475318f --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs @@ -0,0 +1,67 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct IndexType : IStringEnum +{ + public static readonly IndexType IndexTypeInvalid = new(Values.IndexTypeInvalid); + + public static readonly IndexType IndexTypeDefault = new(Values.IndexTypeDefault); + + public static readonly IndexType IndexTypeStrict = new(Values.IndexTypeStrict); + + public IndexType(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static IndexType FromCustom(string value) + { + return new IndexType(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(IndexType value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(IndexType value1, string value2) => !value1.Value.Equals(value2); + + public static explicit operator string(IndexType value) => value.Value; + + public static explicit operator IndexType(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string IndexTypeInvalid = "INDEX_TYPE_INVALID"; + + public const string IndexTypeDefault = "INDEX_TYPE_DEFAULT"; + + public const string IndexTypeStrict = "INDEX_TYPE_STRICT"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApi.csproj b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApi.csproj index 4bac237048e3..70240c028c83 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApi.csproj +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/SeedApi.csproj @@ -44,6 +44,39 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Column.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Column.cs new file mode 100644 index 000000000000..05b06d26bbdc --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Column.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Column : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("values")] + public ReadOnlyMemory Values { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Column type from its Protobuf-equivalent representation. + /// + internal static Column FromProto(ProtoDataV1Grpc.Column value) + { + return new Column + { + Id = value.Id, + Values = value.Values?.ToArray() ?? new ReadOnlyMemory(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Column type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Column ToProto() + { + var result = new ProtoDataV1Grpc.Column(); + result.Id = Id; + if (!Values.IsEmpty) + { + result.Values.AddRange(Values.ToArray()); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DeleteResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DeleteResponse.cs new file mode 100644 index 000000000000..2c72cc2beea7 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DeleteResponse.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DeleteResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DeleteResponse type from its Protobuf-equivalent representation. + /// + internal static DeleteResponse FromProto(ProtoDataV1Grpc.DeleteResponse value) + { + return new DeleteResponse(); + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DeleteResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DeleteResponse ToProto() + { + return new ProtoDataV1Grpc.DeleteResponse(); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DescribeResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DescribeResponse.cs new file mode 100644 index 000000000000..be7b2a6f4b01 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/DescribeResponse.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record DescribeResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("namespaces")] + public Dictionary? Namespaces { get; set; } + + [JsonPropertyName("dimension")] + public uint? Dimension { get; set; } + + [JsonPropertyName("fullness")] + public float? Fullness { get; set; } + + [JsonPropertyName("total_count")] + public uint? TotalCount { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new DescribeResponse type from its Protobuf-equivalent representation. + /// + internal static DescribeResponse FromProto(ProtoDataV1Grpc.DescribeResponse value) + { + return new DescribeResponse + { + Namespaces = value.Namespaces?.ToDictionary( + kvp => kvp.Key, + kvp => NamespaceSummary.FromProto(kvp.Value) + ), + Dimension = value.Dimension, + Fullness = value.Fullness, + TotalCount = value.TotalCount, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the DescribeResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.DescribeResponse ToProto() + { + var result = new ProtoDataV1Grpc.DescribeResponse(); + if (Namespaces != null && Namespaces.Any()) + { + foreach (var kvp in Namespaces) + { + result.Namespaces.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Dimension != null) + { + result.Dimension = Dimension ?? 0; + } + if (Fullness != null) + { + result.Fullness = Fullness ?? 0.0f; + } + if (TotalCount != null) + { + result.TotalCount = TotalCount ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FetchResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FetchResponse.cs new file mode 100644 index 000000000000..0558b91da66e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FetchResponse.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record FetchResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public Dictionary? Columns { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new FetchResponse type from its Protobuf-equivalent representation. + /// + internal static FetchResponse FromProto(ProtoDataV1Grpc.FetchResponse value) + { + return new FetchResponse + { + Columns = value.Columns?.ToDictionary( + kvp => kvp.Key, + kvp => Column.FromProto(kvp.Value) + ), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the FetchResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.FetchResponse ToProto() + { + var result = new ProtoDataV1Grpc.FetchResponse(); + if (Columns != null && Columns.Any()) + { + foreach (var kvp in Columns) + { + result.Columns.Add(kvp.Key, kvp.Value.ToProto()); + } + ; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs new file mode 100644 index 000000000000..38cc250adbcf --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs @@ -0,0 +1,95 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; + +namespace SeedApi; + +[JsonConverter(typeof(StringEnumSerializer))] +[Serializable] +public readonly record struct FieldBehavior : IStringEnum +{ + public static readonly FieldBehavior FieldBehaviorUnspecified = new( + Values.FieldBehaviorUnspecified + ); + + public static readonly FieldBehavior Optional = new(Values.Optional); + + public static readonly FieldBehavior Required = new(Values.Required); + + public static readonly FieldBehavior OutputOnly = new(Values.OutputOnly); + + public static readonly FieldBehavior InputOnly = new(Values.InputOnly); + + public static readonly FieldBehavior Immutable = new(Values.Immutable); + + public static readonly FieldBehavior UnorderedList = new(Values.UnorderedList); + + public static readonly FieldBehavior NonEmptyDefault = new(Values.NonEmptyDefault); + + public static readonly FieldBehavior Identifier = new(Values.Identifier); + + public FieldBehavior(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + /// + /// Create a string enum with the given value. + /// + public static FieldBehavior FromCustom(string value) + { + return new FieldBehavior(value); + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public static bool operator ==(FieldBehavior value1, string value2) => + value1.Value.Equals(value2); + + public static bool operator !=(FieldBehavior value1, string value2) => + !value1.Value.Equals(value2); + + public static explicit operator string(FieldBehavior value) => value.Value; + + public static explicit operator FieldBehavior(string value) => new(value); + + /// + /// Constant strings for enum values + /// + [Serializable] + public static class Values + { + public const string FieldBehaviorUnspecified = "FIELD_BEHAVIOR_UNSPECIFIED"; + + public const string Optional = "OPTIONAL"; + + public const string Required = "REQUIRED"; + + public const string OutputOnly = "OUTPUT_ONLY"; + + public const string InputOnly = "INPUT_ONLY"; + + public const string Immutable = "IMMUTABLE"; + + public const string UnorderedList = "UNORDERED_LIST"; + + public const string NonEmptyDefault = "NON_EMPTY_DEFAULT"; + + public const string Identifier = "IDENTIFIER"; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/IndexedData.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/IndexedData.cs new file mode 100644 index 000000000000..950c877f2c98 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/IndexedData.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record IndexedData : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("indices")] + public IEnumerable Indices { get; set; } = new List(); + + [JsonPropertyName("values")] + public ReadOnlyMemory Values { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new IndexedData type from its Protobuf-equivalent representation. + /// + internal static IndexedData FromProto(ProtoDataV1Grpc.IndexedData value) + { + return new IndexedData + { + Indices = value.Indices?.ToList() ?? Enumerable.Empty(), + Values = value.Values?.ToArray() ?? new ReadOnlyMemory(), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the IndexedData type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.IndexedData ToProto() + { + var result = new ProtoDataV1Grpc.IndexedData(); + if (Indices.Any()) + { + result.Indices.AddRange(Indices); + } + if (!Values.IsEmpty) + { + result.Values.AddRange(Values.ToArray()); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListElement.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListElement.cs new file mode 100644 index 000000000000..ff0e19b9e9a9 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListElement.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListElement : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListElement type from its Protobuf-equivalent representation. + /// + internal static ListElement FromProto(ProtoDataV1Grpc.ListElement value) + { + return new ListElement { Id = value.Id }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListElement type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListElement ToProto() + { + var result = new ProtoDataV1Grpc.ListElement(); + if (Id != null) + { + result.Id = Id ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListResponse.cs new file mode 100644 index 000000000000..72b63f8287fd --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ListResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ListResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("columns")] + public IEnumerable? Columns { get; set; } + + [JsonPropertyName("pagination")] + public Pagination? Pagination { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ListResponse type from its Protobuf-equivalent representation. + /// + internal static ListResponse FromProto(ProtoDataV1Grpc.ListResponse value) + { + return new ListResponse + { + Columns = value.Columns?.Select(ListElement.FromProto), + Pagination = value.Pagination != null ? Pagination.FromProto(value.Pagination) : null, + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ListResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ListResponse ToProto() + { + var result = new ProtoDataV1Grpc.ListResponse(); + if (Columns != null && Columns.Any()) + { + result.Columns.AddRange(Columns.Select(elem => elem.ToProto())); + } + if (Pagination != null) + { + result.Pagination = Pagination.ToProto(); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Metadata.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Metadata.cs new file mode 100644 index 000000000000..a767a7f0bdd6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Metadata.cs @@ -0,0 +1,39 @@ +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } + + internal static Metadata FromProto(WellKnownProto.Struct value) + { + var result = new Metadata(); + foreach (var kvp in value.Fields) + { + result[kvp.Key] = kvp.Value != null ? MetadataValue.FromProto(kvp.Value) : null; + } + return result; + } + + internal WellKnownProto.Struct ToProto() + { + var result = new WellKnownProto.Struct(); + foreach (var kvp in this) + { + result.Fields[kvp.Key] = kvp.Value?.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/MetadataValue.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/MetadataValue.cs new file mode 100644 index 000000000000..51b7b46595b3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/MetadataValue.cs @@ -0,0 +1,91 @@ +using OneOf; +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class MetadataValue( + OneOf, Metadata> value +) : OneOfBase, Metadata>(value) +{ + internal static MetadataValue? FromProto(WellKnownProto.Value value) + { + return value.KindCase switch + { + WellKnownProto.Value.KindOneofCase.StringValue => value.StringValue, + WellKnownProto.Value.KindOneofCase.NumberValue => value.NumberValue, + WellKnownProto.Value.KindOneofCase.BoolValue => value.BoolValue, + WellKnownProto.Value.KindOneofCase.ListValue => value + .ListValue.Values.Select(FromProto) + .ToList(), + WellKnownProto.Value.KindOneofCase.StructValue => Metadata.FromProto(value.StructValue), + _ => null, + }; + } + + internal WellKnownProto.Value ToProto() + { + return Match( + WellKnownProto.Value.ForString, + WellKnownProto.Value.ForNumber, + WellKnownProto.Value.ForBool, + list => new WellKnownProto.Value + { + ListValue = new WellKnownProto.ListValue + { + Values = { list.Select(item => item?.ToProto()) }, + }, + }, + nested => new WellKnownProto.Value { StructValue = nested.ToProto() } + ); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } + + public static implicit operator MetadataValue(string value) => new(value); + + public static implicit operator MetadataValue(bool value) => new(value); + + public static implicit operator MetadataValue(double value) => new(value); + + public static implicit operator MetadataValue(Metadata value) => new(value); + + public static implicit operator MetadataValue(MetadataValue?[] value) => new(value); + + public static implicit operator MetadataValue(List value) => new(value); + + public static implicit operator MetadataValue(string[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(bool[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(bool?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/NamespaceSummary.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/NamespaceSummary.cs new file mode 100644 index 000000000000..b302c01763b5 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/NamespaceSummary.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record NamespaceSummary : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new NamespaceSummary type from its Protobuf-equivalent representation. + /// + internal static NamespaceSummary FromProto(ProtoDataV1Grpc.NamespaceSummary value) + { + return new NamespaceSummary { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the NamespaceSummary type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.NamespaceSummary ToProto() + { + var result = new ProtoDataV1Grpc.NamespaceSummary(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Pagination.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Pagination.cs new file mode 100644 index 000000000000..2eb1d1eeedec --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Pagination.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Pagination : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("next")] + public string? Next { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Pagination type from its Protobuf-equivalent representation. + /// + internal static Pagination FromProto(ProtoDataV1Grpc.Pagination value) + { + return new Pagination { Next = value.Next }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Pagination type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Pagination ToProto() + { + var result = new ProtoDataV1Grpc.Pagination(); + if (Next != null) + { + result.Next = Next ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryColumn.cs new file mode 100644 index 000000000000..7120dfef5ed1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryColumn.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("values")] + public ReadOnlyMemory Values { get; set; } + + [JsonPropertyName("top_k")] + public uint? TopK { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("filter")] + public Metadata? Filter { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryColumn type from its Protobuf-equivalent representation. + /// + internal static QueryColumn FromProto(ProtoDataV1Grpc.QueryColumn value) + { + return new QueryColumn + { + Values = value.Values?.ToArray() ?? new ReadOnlyMemory(), + TopK = value.TopK, + Namespace = value.Namespace, + Filter = value.Filter != null ? Metadata.FromProto(value.Filter) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryColumn ToProto() + { + var result = new ProtoDataV1Grpc.QueryColumn(); + if (!Values.IsEmpty) + { + result.Values.AddRange(Values.ToArray()); + } + if (TopK != null) + { + result.TopK = TopK ?? 0; + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Filter != null) + { + result.Filter = Filter.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResponse.cs new file mode 100644 index 000000000000..9242ccd094a1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResponse.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("results")] + public IEnumerable? Results { get; set; } + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResponse type from its Protobuf-equivalent representation. + /// + internal static QueryResponse FromProto(ProtoDataV1Grpc.QueryResponse value) + { + return new QueryResponse + { + Results = value.Results?.Select(QueryResult.FromProto), + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + Usage = value.Usage != null ? Usage.FromProto(value.Usage) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResponse ToProto() + { + var result = new ProtoDataV1Grpc.QueryResponse(); + if (Results != null && Results.Any()) + { + result.Results.AddRange(Results.Select(elem => elem.ToProto())); + } + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + if (Usage != null) + { + result.Usage = Usage.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResult.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResult.cs new file mode 100644 index 000000000000..8a0ab47d124a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/QueryResult.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record QueryResult : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("matches")] + public IEnumerable? Matches { get; set; } + + [JsonPropertyName("namespace")] + public string? Namespace { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new QueryResult type from its Protobuf-equivalent representation. + /// + internal static QueryResult FromProto(ProtoDataV1Grpc.QueryResult value) + { + return new QueryResult + { + Matches = value.Matches?.Select(ScoredColumn.FromProto), + Namespace = value.Namespace, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the QueryResult type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.QueryResult ToProto() + { + var result = new ProtoDataV1Grpc.QueryResult(); + if (Matches != null && Matches.Any()) + { + result.Matches.AddRange(Matches.Select(elem => elem.ToProto())); + } + if (Namespace != null) + { + result.Namespace = Namespace ?? ""; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ScoredColumn.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ScoredColumn.cs new file mode 100644 index 000000000000..80499319efe7 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/ScoredColumn.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record ScoredColumn : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("score")] + public float? Score { get; set; } + + [JsonPropertyName("values")] + public ReadOnlyMemory? Values { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonPropertyName("indexed_data")] + public IndexedData? IndexedData { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new ScoredColumn type from its Protobuf-equivalent representation. + /// + internal static ScoredColumn FromProto(ProtoDataV1Grpc.ScoredColumn value) + { + return new ScoredColumn + { + Id = value.Id, + Score = value.Score, + Values = value.Values?.ToArray(), + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + IndexedData = + value.IndexedData != null ? IndexedData.FromProto(value.IndexedData) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the ScoredColumn type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.ScoredColumn ToProto() + { + var result = new ProtoDataV1Grpc.ScoredColumn(); + result.Id = Id; + if (Score != null) + { + result.Score = Score ?? 0.0f; + } + if (Values != null && !Values.Value.IsEmpty) + { + result.Values.AddRange(Values.Value.ToArray()); + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + if (IndexedData != null) + { + result.IndexedData = IndexedData.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UpdateResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UpdateResponse.cs new file mode 100644 index 000000000000..643f8a79e161 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UpdateResponse.cs @@ -0,0 +1,116 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public record UpdateResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("updated_at")] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("index_type")] + public IndexType? IndexType { get; set; } + + [JsonPropertyName("details")] + public object? Details { get; set; } + + [JsonPropertyName("index_types")] + public IEnumerable? IndexTypes { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UpdateResponse type from its Protobuf-equivalent representation. + /// + internal static UpdateResponse FromProto(ProtoDataV1Grpc.UpdateResponse value) + { + return new UpdateResponse + { + UpdatedAt = value.UpdatedAt.ToDateTime(), + IndexType = value.IndexType switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexType}"), + }, + Details = value.Details != null ? value.Details : null, + IndexTypes = value.IndexTypes.Select(type => + type switch + { + ProtoDataV1Grpc.IndexType.Invalid => SeedApi.IndexType.IndexTypeInvalid, + ProtoDataV1Grpc.IndexType.Default => SeedApi.IndexType.IndexTypeDefault, + ProtoDataV1Grpc.IndexType.Strict => SeedApi.IndexType.IndexTypeStrict, + _ => throw new ArgumentException($"Unknown enum value: {value.IndexTypes}"), + } + ), + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UpdateResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UpdateResponse ToProto() + { + var result = new ProtoDataV1Grpc.UpdateResponse(); + if (UpdatedAt != null) + { + result.UpdatedAt = WellKnownProto.Timestamp.FromDateTime( + UpdatedAt.Value.ToUniversalTime() + ); + } + if (IndexType != null) + { + result.IndexType = IndexType.Value.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc.IndexType.Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc.IndexType.Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc.IndexType.Strict, + _ => throw new ArgumentException($"Unknown enum value: {IndexType.Value.Value}"), + }; + } + if (Details != null) + { + result.Details = ProtoAnyMapper.ToProto(Details); + } + if (IndexTypes != null && IndexTypes.Any()) + { + result.IndexTypes.AddRange( + IndexTypes.Select(type => + type.Value switch + { + SeedApi.IndexType.Values.IndexTypeInvalid => ProtoDataV1Grpc + .IndexType + .Invalid, + SeedApi.IndexType.Values.IndexTypeDefault => ProtoDataV1Grpc + .IndexType + .Default, + SeedApi.IndexType.Values.IndexTypeStrict => ProtoDataV1Grpc + .IndexType + .Strict, + _ => throw new ArgumentException($"Unknown enum value: {type}"), + } + ) + ); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UploadResponse.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UploadResponse.cs new file mode 100644 index 000000000000..2409ef7d7bff --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/UploadResponse.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record UploadResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("count")] + public uint? Count { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UploadResponse type from its Protobuf-equivalent representation. + /// + internal static UploadResponse FromProto(ProtoDataV1Grpc.UploadResponse value) + { + return new UploadResponse { Count = value.Count }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UploadResponse type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.UploadResponse ToProto() + { + var result = new ProtoDataV1Grpc.UploadResponse(); + if (Count != null) + { + result.Count = Count ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Usage.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Usage.cs new file mode 100644 index 000000000000..c6c1ccf37d4a --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/Usage.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoDataV1Grpc = Data.V1.Grpc; + +namespace SeedApi; + +[Serializable] +public record Usage : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("units")] + public uint? Units { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new Usage type from its Protobuf-equivalent representation. + /// + internal static Usage FromProto(ProtoDataV1Grpc.Usage value) + { + return new Usage { Units = value.Units }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the Usage type into its Protobuf-equivalent representation. + /// + internal ProtoDataV1Grpc.Usage ToProto() + { + var result = new ProtoDataV1Grpc.Usage(); + if (Units != null) + { + result.Units = Units ?? 0; + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/.editorconfig b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.editorconfig similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/.editorconfig rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.editorconfig diff --git a/seed/csharp-sdk/csharp-grpc-proto/.fern/metadata.json b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.fern/metadata.json similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/.fern/metadata.json rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.fern/metadata.json diff --git a/seed/csharp-sdk/csharp-grpc-proto/.github/workflows/ci.yml b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.github/workflows/ci.yml similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/.github/workflows/ci.yml rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.github/workflows/ci.yml diff --git a/seed/csharp-sdk/csharp-grpc-proto/.gitignore b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.gitignore similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/.gitignore rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/.gitignore diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/README.md b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/README.md new file mode 100644 index 000000000000..084b952a5cda --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/README.md @@ -0,0 +1,124 @@ +# Seed C# Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FC%23) +[![nuget shield](https://img.shields.io/nuget/v/SeedApi)](https://nuget.org/packages/SeedApi) + +The Seed C# library provides convenient access to the Seed APIs from C#. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Additional Headers](#additional-headers) +- [Contributing](#contributing) + +## Requirements + +This SDK requires: + +## Installation + +```sh +dotnet add package SeedApi +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```csharp +using SeedApi; + +var client = new SeedApiClient(); +await client.Userservice.CreateAsync(new CreateRequest()); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```csharp +using SeedApi; + +try { + var response = await client.Userservice.CreateAsync(...); +} catch (SeedApiApiException e) { + System.Console.WriteLine(e.Body); + System.Console.WriteLine(e.StatusCode); +} +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `MaxRetries` request option to configure this behavior. + +```csharp +var response = await client.Userservice.CreateAsync( + ..., + new RequestOptions { + MaxRetries: 0 // Override MaxRetries at the request level + } +); +``` + +### Timeouts + +The SDK defaults to a 30 second timeout. Use the `Timeout` option to configure this behavior. + +```csharp +var response = await client.Userservice.CreateAsync( + ..., + new RequestOptions { + Timeout: TimeSpan.FromSeconds(3) // Override timeout to 3s + } +); +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. + +```csharp +var response = await client.Userservice.CreateAsync( + ..., + new RequestOptions { + AdditionalHeaders = new Dictionary + { + { "X-Custom-Header", "custom-value" } + } + } +); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/csharp-sdk/csharp-grpc-proto/SeedApi.slnx b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/SeedApi.slnx similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/SeedApi.slnx rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/SeedApi.slnx diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/annotations.proto b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/annotations.proto new file mode 100644 index 000000000000..8ff42098404c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/http.proto b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/http.proto new file mode 100644 index 000000000000..c8392381eb99 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/google/api/http.proto @@ -0,0 +1,379 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/user/v1/user.proto b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/user/v1/user.proto new file mode 100644 index 000000000000..28542ac965a1 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/proto/user/v1/user.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package user.v1; + +import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; + +option csharp_namespace = "User.V1"; +option go_package = "user/v1"; + +message UserModel { + string username = 1; + string email = 2; + uint32 age = 3; + float weight = 4; + google.protobuf.Struct metadata = 5; +} + +message CreateRequest { + string username = 1; + string email = 2; + uint32 age = 3; + float weight = 4; + google.protobuf.Struct metadata = 5; +} + +message CreateResponse { + UserModel user = 1; +} + +service UserService { + rpc Create(CreateRequest) returns (CreateResponse) { + option (google.api.http) = { + post: "/users" + body: "*" + }; + } +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/reference.md b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/reference.md new file mode 100644 index 000000000000..2a8af3906abc --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/reference.md @@ -0,0 +1,42 @@ +# Reference +## UserService +
client.Userservice.CreateAsync(CreateRequest { ... }) -> WithRawResponseTask<CreateResponse> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.Userservice.CreateAsync(new CreateRequest()); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `CreateRequest` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/snippet.json b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/snippet.json new file mode 100644 index 000000000000..df65366f4528 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/snippet.json @@ -0,0 +1,17 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": null, + "id": { + "path": "/users", + "method": "POST", + "identifier_override": "endpoint_userservice.create" + }, + "snippet": { + "type": "csharp", + "client": "using SeedApi;\n\nvar client = new SeedApiClient();\nawait client.Userservice.CreateAsync(new CreateRequest());\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/HeadersBuilderTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/HeadersBuilderTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/HeadersBuilderTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/HeadersBuilderTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/AdditionalPropertiesTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/AdditionalPropertiesTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/AdditionalPropertiesTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/AdditionalPropertiesTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/DateOnlyJsonTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/DateOnlyJsonTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/DateOnlyJsonTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/DateOnlyJsonTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/DateTimeJsonTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/DateTimeJsonTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/DateTimeJsonTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/DateTimeJsonTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/QueryStringBuilderTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/QueryStringBuilderTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/QueryStringBuilderTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/QueryStringBuilderTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/QueryStringConverterTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/QueryStringConverterTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/QueryStringConverterTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/QueryStringConverterTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/RawClientTests/MultipartFormTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/MultipartFormTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/RawClientTests/MultipartFormTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/MultipartFormTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/RawClientTests/QueryParameterTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/QueryParameterTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/RawClientTests/QueryParameterTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/QueryParameterTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/WithRawResponseTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/WithRawResponseTests.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/WithRawResponseTests.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/WithRawResponseTests.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/SeedApi.Test.Custom.props b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/SeedApi.Test.Custom.props similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/SeedApi.Test.Custom.props rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/SeedApi.Test.Custom.props diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/SeedApi.Test.csproj b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/SeedApi.Test.csproj similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/SeedApi.Test.csproj rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/SeedApi.Test.csproj diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/TestClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/TestClient.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/TestClient.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/TestClient.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/AdditionalPropertiesComparer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/AdditionalPropertiesComparer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/AdditionalPropertiesComparer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/AdditionalPropertiesComparer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/JsonAssert.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/JsonAssert.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/JsonAssert.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/JsonAssert.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/JsonElementComparer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/JsonElementComparer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/JsonElementComparer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/JsonElementComparer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/NUnitExtensions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/NUnitExtensions.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/NUnitExtensions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/NUnitExtensions.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/OneOfComparer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/OneOfComparer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/OneOfComparer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/OneOfComparer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/OptionalComparer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/OptionalComparer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/OptionalComparer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/OptionalComparer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/ReadOnlyMemoryComparer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/ReadOnlyMemoryComparer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Utils/ReadOnlyMemoryComparer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Utils/ReadOnlyMemoryComparer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/ApiResponse.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/ApiResponse.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/ApiResponse.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/ApiResponse.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/BaseRequest.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/BaseRequest.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/BaseRequest.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/BaseRequest.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/CollectionItemSerializer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/CollectionItemSerializer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/CollectionItemSerializer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Constants.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Constants.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Constants.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Constants.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/DateOnlyConverter.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/DateOnlyConverter.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/DateOnlyConverter.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/DateOnlyConverter.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/DateTimeSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/DateTimeSerializer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/DateTimeSerializer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/DateTimeSerializer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/EmptyRequest.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/EmptyRequest.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/EmptyRequest.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/EmptyRequest.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/EncodingCache.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/EncodingCache.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/EncodingCache.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/EncodingCache.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Extensions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Extensions.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Extensions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Extensions.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/FormUrlEncoder.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/FormUrlEncoder.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/FormUrlEncoder.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/FormUrlEncoder.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HeaderValue.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HeaderValue.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HeaderValue.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HeaderValue.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Headers.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Headers.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Headers.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Headers.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HeadersBuilder.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HeadersBuilder.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HeadersBuilder.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HeadersBuilder.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HttpContentExtensions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HttpContentExtensions.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HttpContentExtensions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HttpContentExtensions.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HttpMethodExtensions.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/HttpMethodExtensions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/HttpMethodExtensions.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/IIsRetryableContent.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/IIsRetryableContent.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/IIsRetryableContent.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/IIsRetryableContent.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/IRequestOptions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/IRequestOptions.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/IRequestOptions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/IRequestOptions.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonRequest.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/JsonRequest.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonRequest.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/JsonRequest.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/MultipartFormRequest.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/MultipartFormRequest.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/MultipartFormRequest.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/MultipartFormRequest.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/NullableAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/NullableAttribute.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/NullableAttribute.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/NullableAttribute.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/OneOfSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/OneOfSerializer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/OneOfSerializer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/OneOfSerializer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Optional.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Optional.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Optional.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Optional.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/OptionalAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/OptionalAttribute.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/OptionalAttribute.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/OptionalAttribute.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/AdditionalProperties.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/AdditionalProperties.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/AdditionalProperties.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/AdditionalProperties.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/ClientOptions.cs similarity index 88% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/ClientOptions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/ClientOptions.cs index c073e763ca91..8dcbbd5ecf1d 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/ClientOptions.cs +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/ClientOptions.cs @@ -1,3 +1,4 @@ +using Grpc.Net.Client; using SeedApi.Core; namespace SeedApi; @@ -66,6 +67,17 @@ public partial class ClientOptions #endif } = TimeSpan.FromSeconds(30); + /// + /// The options used for gRPC client endpoints. + /// + public GrpcChannelOptions? GrpcOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + /// /// Clones this and returns a new instance /// diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/FileParameter.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/FileParameter.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/FileParameter.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/FileParameter.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs new file mode 100644 index 000000000000..4fb311172634 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/GrpcRequestOptions.cs @@ -0,0 +1,63 @@ +using Grpc.Core; +using SeedApi.Core; + +namespace SeedApi; + +public partial class GrpcRequestOptions +{ + /// + /// The maximum number of retry attempts. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Options for write operations. + /// + public WriteOptions? WriteOptions { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Client-side call credentials. Provide authorization with per-call granularity. + /// + public CallCredentials? CallCredentials { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with this particular request. + /// Headers with matching keys will be overwritten by headers set on the client options. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new List>(); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/RawResponse.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/RawResponse.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/RawResponse.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/RawResponse.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/RequestOptions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/RequestOptions.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/RequestOptions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/RequestOptions.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/SeedApiApiException.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/SeedApiApiException.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/SeedApiApiException.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/SeedApiApiException.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/SeedApiException.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/SeedApiException.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/SeedApiException.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/SeedApiException.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/Version.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/Version.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/Version.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/Version.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/WithRawResponse.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/WithRawResponse.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/WithRawResponse.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/WithRawResponse.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/WithRawResponseTask.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/WithRawResponseTask.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/Public/WithRawResponseTask.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/Public/WithRawResponseTask.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/QueryStringBuilder.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/QueryStringBuilder.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/QueryStringBuilder.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/QueryStringBuilder.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/QueryStringConverter.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/QueryStringConverter.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/QueryStringConverter.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/QueryStringConverter.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/RawClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawClient.cs similarity index 98% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/RawClient.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawClient.cs index d42791fcc0e0..3e191c04dbe4 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/RawClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawClient.cs @@ -20,6 +20,13 @@ internal partial class RawClient(ClientOptions clientOptions) #endif internal int BaseRetryDelay { get; set; } = 1000; + private readonly Lazy _grpc = new(() => new RawGrpcClient(clientOptions)); + + /// + /// The gRPC client used to make requests. + /// + public RawGrpcClient Grpc => _grpc.Value; + /// /// The client options applied on every request. /// diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs new file mode 100644 index 000000000000..2326d6b36f8c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawGrpcClient.cs @@ -0,0 +1,61 @@ +using Grpc.Core; +using Grpc.Net.Client; + +namespace SeedApi.Core; + +/// +/// Utility class for making gRPC requests to the API. +/// +internal class RawGrpcClient +{ + /// + /// The gRPC channel used to make requests. + /// + public readonly GrpcChannel Channel; + + private readonly ClientOptions _clientOptions; + + public RawGrpcClient(ClientOptions clientOptions) + { + _clientOptions = clientOptions; + + var grpcOptions = PrepareGrpcChannelOptions(); + Channel = grpcOptions is not null + ? GrpcChannel.ForAddress(_clientOptions.BaseUrl, grpcOptions) + : GrpcChannel.ForAddress(_clientOptions.BaseUrl); + } + + /// + /// Creates CallOptions for a gRPC request with the provided metadata, timeout, and credentials. + /// Metadata (headers) should be built at the endpoint level before calling this method. + /// + public CallOptions CreateCallOptions( + global::Grpc.Core.Metadata metadata, + GrpcRequestOptions options, + CancellationToken cancellationToken = default + ) + { + var timeout = options.Timeout ?? _clientOptions.Timeout; + var deadline = DateTime.UtcNow.Add(timeout); + return new CallOptions( + metadata, + deadline, + cancellationToken, + options.WriteOptions, + null, + options.CallCredentials + ); + } + + private GrpcChannelOptions? PrepareGrpcChannelOptions() + { + var grpcChannelOptions = _clientOptions.GrpcOptions; + if (grpcChannelOptions is null) + { + return null; + } + grpcChannelOptions.HttpClient ??= _clientOptions.HttpClient; + grpcChannelOptions.MaxRetryAttempts ??= _clientOptions.MaxRetries; + return grpcChannelOptions; + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/RawResponse.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawResponse.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/RawResponse.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/RawResponse.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/ResponseHeaders.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/ResponseHeaders.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/ResponseHeaders.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/ResponseHeaders.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StreamRequest.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StreamRequest.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StreamRequest.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StreamRequest.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StringEnum.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnum.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StringEnum.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnum.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StringEnumExtensions.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumExtensions.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StringEnumExtensions.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumExtensions.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/StringEnumSerializer.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/ValueConvert.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/ValueConvert.cs similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/ValueConvert.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/ValueConvert.cs diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/ISeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/ISeedApiClient.cs new file mode 100644 index 000000000000..ffefaf871345 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/ISeedApiClient.cs @@ -0,0 +1,6 @@ +namespace SeedApi; + +public partial interface ISeedApiClient +{ + public IUserserviceClient Userservice { get; } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApi.Custom.props b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApi.Custom.props similarity index 100% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApi.Custom.props rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApi.Custom.props diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApi.csproj b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApi.csproj similarity index 73% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApi.csproj rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApi.csproj index d4d3cf0ae0db..7f0abbb33f49 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApi.csproj +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApi.csproj @@ -44,6 +44,34 @@ + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApiClient.cs similarity index 89% rename from seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApiClient.cs rename to seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApiClient.cs index f54c4b761238..ad90c2bb3a65 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/SeedApiClient.cs +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/SeedApiClient.cs @@ -26,5 +26,8 @@ public SeedApiClient(ClientOptions? clientOptions = null) } } _client = new RawClient(clientOptions); + Userservice = new UserserviceClient(_client); } + + public IUserserviceClient Userservice { get; } } diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/CreateResponse.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/CreateResponse.cs new file mode 100644 index 000000000000..9e0d9e26441c --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/CreateResponse.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoUserV1 = User.V1; + +namespace SeedApi; + +[Serializable] +public record CreateResponse : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("user")] + public UserModel? User { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new CreateResponse type from its Protobuf-equivalent representation. + /// + internal static CreateResponse FromProto(ProtoUserV1.CreateResponse value) + { + return new CreateResponse + { + User = value.User != null ? UserModel.FromProto(value.User) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the CreateResponse type into its Protobuf-equivalent representation. + /// + internal ProtoUserV1.CreateResponse ToProto() + { + var result = new ProtoUserV1.CreateResponse(); + if (User != null) + { + result.User = User.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/Metadata.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/Metadata.cs new file mode 100644 index 000000000000..a767a7f0bdd6 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/Metadata.cs @@ -0,0 +1,39 @@ +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } + + internal static Metadata FromProto(WellKnownProto.Struct value) + { + var result = new Metadata(); + foreach (var kvp in value.Fields) + { + result[kvp.Key] = kvp.Value != null ? MetadataValue.FromProto(kvp.Value) : null; + } + return result; + } + + internal WellKnownProto.Struct ToProto() + { + var result = new WellKnownProto.Struct(); + foreach (var kvp in this) + { + result.Fields[kvp.Key] = kvp.Value?.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/MetadataValue.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/MetadataValue.cs new file mode 100644 index 000000000000..51b7b46595b3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/MetadataValue.cs @@ -0,0 +1,91 @@ +using OneOf; +using SeedApi.Core; +using WellKnownProto = Google.Protobuf.WellKnownTypes; + +namespace SeedApi; + +[Serializable] +public sealed class MetadataValue( + OneOf, Metadata> value +) : OneOfBase, Metadata>(value) +{ + internal static MetadataValue? FromProto(WellKnownProto.Value value) + { + return value.KindCase switch + { + WellKnownProto.Value.KindOneofCase.StringValue => value.StringValue, + WellKnownProto.Value.KindOneofCase.NumberValue => value.NumberValue, + WellKnownProto.Value.KindOneofCase.BoolValue => value.BoolValue, + WellKnownProto.Value.KindOneofCase.ListValue => value + .ListValue.Values.Select(FromProto) + .ToList(), + WellKnownProto.Value.KindOneofCase.StructValue => Metadata.FromProto(value.StructValue), + _ => null, + }; + } + + internal WellKnownProto.Value ToProto() + { + return Match( + WellKnownProto.Value.ForString, + WellKnownProto.Value.ForNumber, + WellKnownProto.Value.ForBool, + list => new WellKnownProto.Value + { + ListValue = new WellKnownProto.ListValue + { + Values = { list.Select(item => item?.ToProto()) }, + }, + }, + nested => new WellKnownProto.Value { StructValue = nested.ToProto() } + ); + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } + + public static implicit operator MetadataValue(string value) => new(value); + + public static implicit operator MetadataValue(bool value) => new(value); + + public static implicit operator MetadataValue(double value) => new(value); + + public static implicit operator MetadataValue(Metadata value) => new(value); + + public static implicit operator MetadataValue(MetadataValue?[] value) => new(value); + + public static implicit operator MetadataValue(List value) => new(value); + + public static implicit operator MetadataValue(string[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(double?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(bool[] value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(bool?[] value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => new MetadataValue(v)).ToList()); + + public static implicit operator MetadataValue(List value) => + new(value.Select(v => v != null ? new MetadataValue(v.Value) : null).ToList()); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/UserModel.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/UserModel.cs new file mode 100644 index 000000000000..22c7cfb30155 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Types/UserModel.cs @@ -0,0 +1,85 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedApi.Core; +using ProtoUserV1 = User.V1; + +namespace SeedApi; + +[Serializable] +public record UserModel : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("username")] + public string? Username { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("age")] + public uint? Age { get; set; } + + [JsonPropertyName("weight")] + public float? Weight { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + /// + /// Returns a new UserModel type from its Protobuf-equivalent representation. + /// + internal static UserModel FromProto(ProtoUserV1.UserModel value) + { + return new UserModel + { + Username = value.Username, + Email = value.Email, + Age = value.Age, + Weight = value.Weight, + Metadata = value.Metadata != null ? Metadata.FromProto(value.Metadata) : null, + }; + } + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + /// Maps the UserModel type into its Protobuf-equivalent representation. + /// + internal ProtoUserV1.UserModel ToProto() + { + var result = new ProtoUserV1.UserModel(); + if (Username != null) + { + result.Username = Username ?? ""; + } + if (Email != null) + { + result.Email = Email ?? ""; + } + if (Age != null) + { + result.Age = Age ?? 0; + } + if (Weight != null) + { + result.Weight = Weight ?? 0.0f; + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/IUserserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/IUserserviceClient.cs new file mode 100644 index 000000000000..948eb13bebe2 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/IUserserviceClient.cs @@ -0,0 +1,10 @@ +namespace SeedApi; + +public partial interface IUserserviceClient +{ + Task CreateAsync( + CreateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ); +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/Requests/CreateRequest.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/Requests/CreateRequest.cs new file mode 100644 index 000000000000..0e8ac25edbb7 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/Requests/CreateRequest.cs @@ -0,0 +1,59 @@ +using System.Text.Json.Serialization; +using SeedApi.Core; +using Proto = User.V1; + +namespace SeedApi; + +[Serializable] +public record CreateRequest +{ + [JsonPropertyName("username")] + public string? Username { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("age")] + public uint? Age { get; set; } + + [JsonPropertyName("weight")] + public float? Weight { get; set; } + + [JsonPropertyName("metadata")] + public Metadata? Metadata { get; set; } + + /// + /// Maps the CreateRequest type into its Protobuf-equivalent representation. + /// + internal Proto.CreateRequest ToProto() + { + var result = new Proto.CreateRequest(); + if (Username != null) + { + result.Username = Username ?? ""; + } + if (Email != null) + { + result.Email = Email ?? ""; + } + if (Age != null) + { + result.Age = Age ?? 0; + } + if (Weight != null) + { + result.Weight = Weight ?? 0.0f; + } + if (Metadata != null) + { + result.Metadata = Metadata.ToProto(); + } + return result; + } + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/UserserviceClient.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/UserserviceClient.cs new file mode 100644 index 000000000000..66fb221f639e --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Userservice/UserserviceClient.cs @@ -0,0 +1,79 @@ +using Grpc.Core; +using SeedApi.Core; +using User.V1; + +namespace SeedApi; + +public partial class UserserviceClient : IUserserviceClient +{ + private readonly RawClient _client; + + private readonly RawGrpcClient _grpc; + + private UserService.UserServiceClient _userService; + + internal UserserviceClient(RawClient client) + { + _client = client; + _grpc = _client.Grpc; + _userService = new UserService.UserServiceClient(_grpc.Channel); + } + + /// + /// await client.Userservice.CreateAsync(new CreateRequest()); + /// + public async Task CreateAsync( + CreateRequest request, + GrpcRequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + try + { + var metadata = new global::Grpc.Core.Metadata(); + foreach (var header in _client.Options.Headers) + { + var value = await header.Value.ResolveAsync().ConfigureAwait(false); + metadata.Add(header.Key, value); + } + if (_client.Options.AdditionalHeaders != null) + { + foreach (var header in _client.Options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + if (options?.AdditionalHeaders != null) + { + foreach (var header in options.AdditionalHeaders) + { + if (header.Value != null) + metadata.Add(header.Key, header.Value); + } + } + + var callOptions = _grpc.CreateCallOptions( + metadata, + options ?? new GrpcRequestOptions(), + cancellationToken + ); + var call = _userService.CreateAsync(request.ToProto(), callOptions); + var response = await call.ConfigureAwait(false); + return CreateResponse.FromProto(response); + } + catch (RpcException rpc) + { + var statusCode = (int)rpc.StatusCode; + throw new SeedApiApiException( + $"Error with gRPC status code {statusCode}", + statusCode, + rpc.Message + ); + } + catch (Exception e) + { + throw new SeedApiException("Error", e); + } + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/snippet.json b/seed/csharp-sdk/csharp-grpc-proto/snippet.json deleted file mode 100644 index a489e9e12dc5..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto/snippet.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "types": {}, - "endpoints": [] -} \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj deleted file mode 100644 index 3417db2e58e2..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - 12 - enable - enable - - - - - - \ No newline at end of file diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/ISeedApiClient.cs b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/ISeedApiClient.cs deleted file mode 100644 index b83c043475f8..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/ISeedApiClient.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SeedApi; - -public partial interface ISeedApiClient { } diff --git a/seed/csharp-sdk/seed.yml b/seed/csharp-sdk/seed.yml index 8ad4b1aa75ac..aebd33da1be9 100644 --- a/seed/csharp-sdk/seed.yml +++ b/seed/csharp-sdk/seed.yml @@ -68,7 +68,10 @@ scripts: fi dotnet build "$solution_file" -c Release - dotnet build src/*DynamicSnippets/*.csproj -c Release + dynamic_snippets="$(find src -maxdepth 2 -name '*DynamicSnippets*.csproj' | head -n 1)" + if [ -n "$dynamic_snippets" ]; then + dotnet build "$dynamic_snippets" -c Release + fi test: - | # Find the solution file @@ -199,19 +202,27 @@ fixtures: - customConfig: inline-path-parameters: false outputFolder: no-inline-path-parameters + csharp-grpc-proto: + - customConfig: null + outputFolder: no-custom-config + disableDynamicSnippetTests: true csharp-grpc-proto-exhaustive: - customConfig: null outputFolder: no-custom-config + disableDynamicSnippetTests: true - customConfig: package-id: Seed.Client outputFolder: package-id + disableDynamicSnippetTests: true - customConfig: read-only-memory-types: - float outputFolder: read-only-memory + disableDynamicSnippetTests: true - customConfig: include-exception-handler: true outputFolder: include-exception-handler + disableDynamicSnippetTests: true oauth-client-credentials: - customConfig: null outputFolder: no-custom-config diff --git a/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/generators.yml b/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/generators.yml index 92167e73272b..2f449cfdf95d 100644 --- a/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/generators.yml +++ b/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/generators.yml @@ -6,6 +6,7 @@ api: root: proto target: proto/data/v1/data.proto overrides: overrides.yml + from-openapi: true local-generation: true groups: php-sdk: diff --git a/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/overrides.yml b/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/overrides.yml index e86e6a909ebf..17e3b51b9b23 100644 --- a/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/overrides.yml +++ b/test-definitions/fern/apis/csharp-grpc-proto-exhaustive/overrides.yml @@ -11,7 +11,7 @@ paths: application/json: schema: properties: - indexType: + index_type: $ref: "#/components/schemas/IndexType" details: $ref: "#/components/schemas/Any" @@ -64,11 +64,11 @@ components: $ref: "#/components/schemas/Metadata" UpdateRequest: properties: - setMetadata: + set_metadata: $ref: "#/components/schemas/Metadata" - indexType: + index_type: $ref: "#/components/schemas/IndexType" - indexTypes: + index_types: type: array items: $ref: "#/components/schemas/IndexType" @@ -76,11 +76,11 @@ components: $ref: "#/components/schemas/Any" UpdateResponse: properties: - indexType: + index_type: $ref: "#/components/schemas/IndexType" details: $ref: "#/components/schemas/Any" - indexTypes: + index_types: type: array items: $ref: "#/components/schemas/IndexType" diff --git a/test-definitions/fern/apis/csharp-grpc-proto/generators.yml b/test-definitions/fern/apis/csharp-grpc-proto/generators.yml index bfe5c2746e66..d0c11ca9a023 100644 --- a/test-definitions/fern/apis/csharp-grpc-proto/generators.yml +++ b/test-definitions/fern/apis/csharp-grpc-proto/generators.yml @@ -5,6 +5,7 @@ api: root: proto target: proto/user/v1/user.proto overrides: overrides.yml + from-openapi: true local-generation: true groups: php-sdk: From 34065b3ccb5706cd54bfa4fdc32d2b95f9b508ea Mon Sep 17 00:00:00 2001 From: patrick thornton <70873350+patrickthornton@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:43:53 -0400 Subject: [PATCH 03/29] fix(csharp): `RetriesTest.cs` uses correct JSON serialization formatting (#13517) * test formatting * oops * format agnostic * Update generators/csharp/base/src/asIs/test/RawClientTests/RetriesTests.Template.cs Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- .../test/RawClientTests/RetriesTests.Template.cs | 13 +++++++------ generators/csharp/sdk/versions.yml | 8 ++++++++ .../Core/RawClientTests/RetriesTests.cs | 4 ++-- .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/generators/csharp/base/src/asIs/test/RawClientTests/RetriesTests.Template.cs b/generators/csharp/base/src/asIs/test/RawClientTests/RetriesTests.Template.cs index 32e9f77803bd..cd5eb0289889 100644 --- a/generators/csharp/base/src/asIs/test/RawClientTests/RetriesTests.Template.cs +++ b/generators/csharp/base/src/asIs/test/RawClientTests/RetriesTests.Template.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using SystemTask = global::System.Threading.Tasks.Task; using WireMock.Server; using WireMockRequest = WireMock.RequestBuilders.Request; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 4f81055b695d..0f6e64500391 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,12 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.27.1 + changelogEntry: + - summary: | + Fix retry test assertions to match the SDK's indented JSON serialization. + type: fix + createdAt: "2026-03-13" + irVersion: 65 + - version: 2.27.0 changelogEntry: - summary: | diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs index fd6241376d35..4005f9f15671 100644 --- a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs @@ -321,7 +321,7 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; + const string expectedBody = "{\n \"key\": \"value\"\n}"; _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) @@ -392,7 +392,7 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() // Verify the retried request preserved the multipart body var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\": \"value\"")); } } diff --git a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs index 9243a5d188c2..76012d71711f 100644 --- a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedUnions.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } From 6d21168933eb278ca4b78d3498647ea1cdf66ccc Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:57:04 -0400 Subject: [PATCH 04/29] chore(csharp): update csharp-sdk seed (#13522) Co-authored-by: patrickthornton --- .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 2 +- .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ .../Core/RawClientTests/RetriesTests.cs | 13 +++++++------ 145 files changed, 1009 insertions(+), 865 deletions(-) diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/RawClientTests/RetriesTests.cs index 85057e4cb5cb..5f167d126f55 100644 --- a/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedAccept.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/RawClientTests/RetriesTests.cs index 6265d594890a..839bc78367ac 100644 --- a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedAliasExtends.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/RawClientTests/RetriesTests.cs index c449567b5de3..f5d27a333e2e 100644 --- a/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedAlias.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/RawClientTests/RetriesTests.cs index a6a8eb8dc2b9..d048fc0d39ec 100644 --- a/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedAnyAuth.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/RawClientTests/RetriesTests.cs index ae2e05de78ad..30b91c3e8d78 100644 --- a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApiWideBasePath.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/RawClientTests/RetriesTests.cs index 0669d368da42..058492220a74 100644 --- a/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedAudiences.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs index b00d51340f7e..dd34ca3bbac9 100644 --- a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedBasicAuthEnvironmentVariables.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs index 4005f9f15671..5e1926b2ae60 100644 --- a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedBasicAuth.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = "{\n \"key\": \"value\"\n}"; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\": \"value\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs index de0fdf0d7186..7edf27e4786f 100644 --- a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedBearerTokenEnvironmentVariable.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/RawClientTests/RetriesTests.cs index 88dad4131aff..6c470e766db6 100644 --- a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedBytesDownload.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/RawClientTests/RetriesTests.cs index 95c31d390b7a..6c2dfdfb7c13 100644 --- a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedBytesUpload.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/RawClientTests/RetriesTests.cs index d98c90c3e1a3..bd2f8a5fe0f7 100644 --- a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedClientSideParams.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/RawClientTests/RetriesTests.cs index fec77d6fd02f..fdf8b727fc67 100644 --- a/seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedContentTypes.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/RawClientTests/RetriesTests.cs index 536a3cc86db8..9d38c41fcb58 100644 --- a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedCrossPackageTypeNames.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/RawClientTests/RetriesTests.cs index a73b6530ba97..9f4e30449d75 100644 --- a/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/RawClientTests/RetriesTests.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using NUnit.Framework; using SeedCsharpNamespaceCollision.Core; using WireMock.Server; @@ -320,8 +321,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -351,9 +350,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -389,9 +389,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/RawClientTests/RetriesTests.cs index 4d4b9dad88ee..ddcbf9d3f6e3 100644 --- a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedCsharpNamespaceConflict.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/RawClientTests/RetriesTests.cs index af4da8639fbf..5ce55a5ee536 100644 --- a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedCsharpReadonlyRequest.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/RawClientTests/RetriesTests.cs index d198d02b047f..a681bd47d391 100644 --- a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedCsharpSystemCollision.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/RawClientTests/RetriesTests.cs index 17c2daabcd58..07da66f888d2 100644 --- a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedCsharpXmlEntities.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/RawClientTests/RetriesTests.cs index 0e5ab4461e62..9a67d1b9bb9b 100644 --- a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedDollarStringExamples.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/RawClientTests/RetriesTests.cs index bfabc9c57ba6..66066ece5532 100644 --- a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedEmptyClients.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/RawClientTests/RetriesTests.cs index e2f62f11bb8e..59009f5e773a 100644 --- a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedEndpointSecurityAuth.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs index 455b59e543a1..b61d1839fcb1 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedEnum.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs index 455b59e543a1..b61d1839fcb1 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedEnum.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/RawClientTests/RetriesTests.cs index 4feeb34ba77c..19a5b4722dfc 100644 --- a/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedErrorProperty.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/errors/src/SeedErrors.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/errors/src/SeedErrors.Test/Core/RawClientTests/RetriesTests.cs index 5112505c88e3..018575ef170f 100644 --- a/seed/csharp-sdk/errors/src/SeedErrors.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/errors/src/SeedErrors.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedErrors.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs index 973148c5a2e2..1933eba122ca 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExamples.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs index 973148c5a2e2..1933eba122ca 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExamples.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs index 7c4b2f1ce83c..d9252c345bda 100644 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExhaustive.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs index 7c4b2f1ce83c..d9252c345bda 100644 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExhaustive.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs index 7c4b2f1ce83c..d9252c345bda 100644 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExhaustive.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs index 7c4b2f1ce83c..d9252c345bda 100644 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExhaustive.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs index 7c4b2f1ce83c..d9252c345bda 100644 --- a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExhaustive.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs index 7c4b2f1ce83c..d9252c345bda 100644 --- a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExhaustive.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/RawClientTests/RetriesTests.cs index e5c3744ecd09..23ed35606e2b 100644 --- a/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExtends.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/RawClientTests/RetriesTests.cs index fe71d5e6e1aa..cac0fc581918 100644 --- a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedExtraProperties.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/RawClientTests/RetriesTests.cs index 1fe94b21b8b7..228aafa296f8 100644 --- a/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedFileDownload.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/RawClientTests/RetriesTests.cs index 6db149cfaa3b..3d67f4af21d5 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedFileUpload.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/folders/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/folders/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/folders/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/folders/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs index 58a5312fa249..99ed89d42027 100644 --- a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedHeaderTokenEnvironmentVariable.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/RawClientTests/RetriesTests.cs index 9e2be2bd8695..a00f9718f618 100644 --- a/seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedHeaderToken.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/RawClientTests/RetriesTests.cs index 221185b98146..8cd6890b1026 100644 --- a/seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedHttpHead.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/RawClientTests/RetriesTests.cs index 8464819806f7..627b8ed0ed49 100644 --- a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedIdempotencyHeaders.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/RawClientTests/RetriesTests.cs index daf19b3cc7e0..7f4b9f9c141c 100644 --- a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedInferredAuthExplicit.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/RawClientTests/RetriesTests.cs index 46f897b1e148..4c88727116d2 100644 --- a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedInferredAuthImplicitApiKey.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/RawClientTests/RetriesTests.cs index 05f36f53b46e..6a346bb853c2 100644 --- a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedInferredAuthImplicitNoExpiry.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs index 956dd2b16369..e785c981e147 100644 --- a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedInferredAuthImplicit.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs index 956dd2b16369..e785c981e147 100644 --- a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedInferredAuthImplicit.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs index 0eb31c0fad9e..a5c2c05a355d 100644 --- a/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedLicense.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs index 0eb31c0fad9e..a5c2c05a355d 100644 --- a/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedLicense.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs index 8df75966df5e..47b678f8e73a 100644 --- a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedLiteral.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs index 8df75966df5e..47b678f8e73a 100644 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedLiteral.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/RawClientTests/RetriesTests.cs index bed120c6d8f6..c341d8cc79c6 100644 --- a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedLiteralsUnions.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/RawClientTests/RetriesTests.cs index 8aa51da6a55c..f45aa82dea6c 100644 --- a/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedMixedCase.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests/RetriesTests.cs index 0db149c3a2d4..8a74467544e6 100644 --- a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedMixedFileDirectory.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/RawClientTests/RetriesTests.cs index a16a9c5f9cc4..80dd36ac41e3 100644 --- a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedMultiLineDocs.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs index c01a1831db4f..b23e12c4900e 100644 --- a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedMultiUrlEnvironmentNoDefault.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs index f7109c25c6d0..418ef3805214 100644 --- a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedMultiUrlEnvironment.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs index f7109c25c6d0..418ef3805214 100644 --- a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedMultiUrlEnvironment.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/RawClientTests/RetriesTests.cs index ec02722320df..a8e16c991a71 100644 --- a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedNoEnvironment.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/RawClientTests/RetriesTests.cs index 153355dfeea8..3ca644460b25 100644 --- a/seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedNoRetries.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs index cafbd031a9df..b02e534698be 100644 --- a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedNullableOptional.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs index cafbd031a9df..b02e534698be 100644 --- a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedNullableOptional.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs index 63df2cd6464a..079bee02e561 100644 --- a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedNullable.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs index 63df2cd6464a..079bee02e561 100644 --- a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedNullable.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs index ba14c84de28c..98a13ffebca6 100644 --- a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentials.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/RawClientTests/RetriesTests.cs index 0413909217d6..02efd8ce7194 100644 --- a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentialsDefault.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs index 9c8220630970..be3f1511df72 100644 --- a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentialsEnvironmentVariables.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/RawClientTests/RetriesTests.cs index 642a06f60106..28ae2d284a21 100644 --- a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentialsMandatoryAuth.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs index ba14c84de28c..98a13ffebca6 100644 --- a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentials.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/RawClientTests/RetriesTests.cs index a755b6b24c86..a659b2093ab1 100644 --- a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentialsReference.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/RawClientTests/RetriesTests.cs index dd5f53af86dd..4af5c195e3d8 100644 --- a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentialsWithVariables.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs index ba14c84de28c..98a13ffebca6 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentials.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs index ba14c84de28c..98a13ffebca6 100644 --- a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedOauthClientCredentials.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/object/src/SeedObject.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/object/src/SeedObject.Test/Core/RawClientTests/RetriesTests.cs index 60b3668957c0..7962f0ace329 100644 --- a/seed/csharp-sdk/object/src/SeedObject.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/object/src/SeedObject.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedObject.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs index 1c55131846b3..9116664010c6 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedObjectsWithImports.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs index 1c55131846b3..9116664010c6 100644 --- a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedObjectsWithImports.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs index 1c55131846b3..9116664010c6 100644 --- a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedObjectsWithImports.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/RawClientTests/RetriesTests.cs index c68710d4309a..5ca0654dbfe7 100644 --- a/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPackageYml.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs index b2be8a2800c3..1409296c4ff4 100644 --- a/seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPagination.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs index b2be8a2800c3..1409296c4ff4 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPagination.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs index b2be8a2800c3..1409296c4ff4 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPagination.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs index b2be8a2800c3..1409296c4ff4 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPagination.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs index 63783fb06824..4bb59a63aed8 100644 --- a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPathParameters.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs index 63783fb06824..4bb59a63aed8 100644 --- a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPathParameters.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/RawClientTests/RetriesTests.cs index 34ce933e36fc..556ea4c8fd71 100644 --- a/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPlainText.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/RawClientTests/RetriesTests.cs index 7095043dd9e1..7601b18aca69 100644 --- a/seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPropertyAccess.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/RawClientTests/RetriesTests.cs index 8667fdecbd2f..fbbfadbe76da 100644 --- a/seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedPublicObject.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/RawClientTests/RetriesTests.cs index 9c592a892a87..3f688262ec65 100644 --- a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedQueryParameters.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs index dadfea93b15f..fe3b586f2eba 100644 --- a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedRequestParameters.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs index dadfea93b15f..fe3b586f2eba 100644 --- a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedRequestParameters.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/RawClientTests/RetriesTests.cs index aa0c94b5a8c9..3de2653bf11d 100644 --- a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedNurseryApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/RawClientTests/RetriesTests.cs index 957d902a90c7..76fd415a1b1e 100644 --- a/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedResponseProperty.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs index fa8b55a8560e..68f00a98c54c 100644 --- a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedServerSentEvents.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs index fa8b55a8560e..68f00a98c54c 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedServerSentEvents.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs index 0c9ef9701755..7ba4a06ea18d 100644 --- a/seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedSimpleApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs index 0c9ef9701755..7ba4a06ea18d 100644 --- a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedSimpleApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs index 0c9ef9701755..7ba4a06ea18d 100644 --- a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedSimpleApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/RawClientTests/RetriesTests.cs index adbcbf0c803c..ef3427d76c49 100644 --- a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedSingleUrlEnvironmentDefault.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs index 0fc89874afd9..81ca675daaee 100644 --- a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedSingleUrlEnvironmentNoDefault.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs index 9fdee25ac757..a1f765282247 100644 --- a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedStreaming.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs index 9fdee25ac757..a1f765282247 100644 --- a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedStreaming.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs index 9fdee25ac757..a1f765282247 100644 --- a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedStreaming.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/RawClientTests/RetriesTests.cs index 0273190990d2..1562f588a0b5 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedTrace.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/RawClientTests/RetriesTests.cs index 4873cbffd0b3..64d51b4fcc0f 100644 --- a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedUndiscriminatedUnionWithResponseProperty.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs index f1873c71c8ed..b84f339b5602 100644 --- a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedUndiscriminatedUnions.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs index f1873c71c8ed..b84f339b5602 100644 --- a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedUndiscriminatedUnions.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs index 9243a5d188c2..984bd49e2209 100644 --- a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedUnions.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs index 76012d71711f..984bd49e2209 100644 --- a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs @@ -353,7 +353,7 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } diff --git a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs index 9243a5d188c2..984bd49e2209 100644 --- a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedUnions.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/RawClientTests/RetriesTests.cs index da9521b6ab24..63d83c2232b9 100644 --- a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedUnknownAsAny.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/RawClientTests/RetriesTests.cs index dcf5a42887d3..86a3806e1589 100644 --- a/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedValidation.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/RawClientTests/RetriesTests.cs index 787ce0a4bf72..217bddc3af96 100644 --- a/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedVariables.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs index 9d988ffaa0ef..bed015a041af 100644 --- a/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedVersion.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/version/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/version/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs index 9d988ffaa0ef..bed015a041af 100644 --- a/seed/csharp-sdk/version/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/version/src/SeedVersion.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedVersion.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs index 1074e6077114..22e8103847cb 100644 --- a/seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/RawClientTests/RetriesTests.cs index 1e202f78b14d..2b367cb5be4f 100644 --- a/seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedWebhooks.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/RawClientTests/RetriesTests.cs index 2723c17dfe56..a5515a48b38e 100644 --- a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedWebsocketBearerAuth.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/RawClientTests/RetriesTests.cs index 05662db4ac11..886c6f57b24e 100644 --- a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedWebsocketAuth.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs index f53ff4095725..bc28b2b2527d 100644 --- a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedWebsocket.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs index f53ff4095725..bc28b2b2527d 100644 --- a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedWebsocket.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } From 8a555b45288ff8d58a4e5da9fa1e60620d1b4148 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:07:31 -0400 Subject: [PATCH 05/29] feat(csharp): generate separate WireMock server per test fixture and parallelize (#13518) * feat(csharp): generate separate WireMock server per test fixture and parallelize Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> * fix: bump version to 2.26.0 (minor for feat change type) Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/csharp/codegen/src/ast/types/TestClass.ts | 10 +++++++++- generators/csharp/codegen/src/context/extern.ts | 8 ++++++++ .../mock-server/BaseMockServerTestGenerator.ts | 7 +------ generators/csharp/sdk/versions.yml | 12 ++++++++++++ .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/EndpointTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/ExtendedInlineRequestBodyTest.cs | 1 + .../Unit/Serialization/ChildTest.cs | 1 + .../Unit/Serialization/ParentTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedAlias.Test/Unit/MockServer/GetTest.cs | 1 + .../SeedAlias.Test/Unit/Serialization/TypeTest.cs | 1 + .../Unit/MockServer/Auth/GetTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetAdminsTest.cs | 1 + .../SeedAnyAuth.Test/Unit/MockServer/User/GetTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/PostTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../FolderA/Service/GetDirectThreadTest.cs | 1 + .../FolderD/Service/GetDirectThreadTest.cs | 1 + .../Unit/MockServer/Foo/FindTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/BasicAuth/GetWithBasicAuthTest.cs | 1 + .../MockServer/BasicAuth/PostWithBasicAuthTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/BasicAuth/GetWithBasicAuthTest.cs | 1 + .../MockServer/BasicAuth/PostWithBasicAuthTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/Service/GetWithBearerTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/SimpleTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/CreateUserTest.cs | 1 + .../Unit/MockServer/Service/DeleteUserTest.cs | 1 + .../Unit/MockServer/Service/GetClientTest.cs | 1 + .../Unit/MockServer/Service/GetConnectionTest.cs | 1 + .../Unit/MockServer/Service/GetResourceTest.cs | 1 + .../Unit/MockServer/Service/GetUserByIdTest.cs | 1 + .../Unit/MockServer/Service/ListClientsTest.cs | 1 + .../Unit/MockServer/Service/ListConnectionsTest.cs | 1 + .../Unit/MockServer/Service/ListResourcesTest.cs | 1 + .../Unit/MockServer/Service/ListUsersTest.cs | 1 + .../Unit/MockServer/Service/SearchResourcesTest.cs | 1 + .../Unit/MockServer/Service/UpdateUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/Service/NamedPatchWithMixedTest.cs | 1 + .../MockServer/Service/OptionalMergePatchTestTest.cs | 1 + .../Unit/MockServer/Service/PatchComplexTest.cs | 1 + .../Unit/MockServer/Service/PatchTest.cs | 1 + .../Unit/MockServer/Service/RegularPatchTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../FolderA/Service/GetDirectThreadTest.cs | 1 + .../FolderD/Service/GetDirectThreadTest.cs | 1 + .../Unit/MockServer/Foo/FindTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/CreateTaskTest.cs | 1 + .../Unit/MockServer/CreateUserTest.cs | 1 + .../Unit/MockServer/System/CreateTaskTest.cs | 1 + .../Unit/MockServer/System/CreateUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Tasktest/HelloTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/BatchCreateTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/CreateTaskTest.cs | 1 + .../Unit/MockServer/CreateUserTest.cs | 1 + .../Unit/MockServer/EmptyResponseTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/GetTimeZoneTest.cs | 1 + .../Unit/MockServer/Auth/GetTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetWithAllAuthTest.cs | 1 + .../Unit/MockServer/User/GetWithAnyAuthTest.cs | 1 + .../Unit/MockServer/User/GetWithApiKeyTest.cs | 1 + .../Unit/MockServer/User/GetWithBasicTest.cs | 1 + .../Unit/MockServer/User/GetWithBearerTest.cs | 1 + .../Unit/MockServer/User/GetWithInferredAuthTest.cs | 1 + .../Unit/MockServer/User/GetWithOAuthTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Headers/SendTest.cs | 1 + .../Unit/MockServer/InlinedRequest/SendTest.cs | 1 + .../Unit/MockServer/PathParam/SendTest.cs | 1 + .../Unit/MockServer/QueryParam/SendListTest.cs | 1 + .../Unit/MockServer/QueryParam/SendTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Headers/SendTest.cs | 1 + .../Unit/MockServer/InlinedRequest/SendTest.cs | 1 + .../Unit/MockServer/PathParam/SendTest.cs | 1 + .../Unit/MockServer/QueryParam/SendListTest.cs | 1 + .../Unit/MockServer/QueryParam/SendTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/PropertyBasedError/ThrowErrorTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Simple/FooTest.cs | 1 + .../Unit/MockServer/Simple/FooWithExamplesTest.cs | 1 + .../MockServer/Simple/FooWithoutEndpointErrorTest.cs | 1 + .../Unit/Serialization/FooRequestTest.cs | 1 + .../Unit/Serialization/FooResponseTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Container/GetAndReturnListOfObjectsTest.cs | 1 + .../Container/GetAndReturnListOfPrimitivesTest.cs | 1 + .../Container/GetAndReturnMapOfPrimToObjectTest.cs | 1 + ...etAndReturnMapOfPrimToUndiscriminatedUnionTest.cs | 1 + .../Container/GetAndReturnMapPrimToPrimTest.cs | 1 + .../Endpoints/Container/GetAndReturnOptionalTest.cs | 1 + .../Container/GetAndReturnSetOfObjectsTest.cs | 1 + .../Container/GetAndReturnSetOfPrimitivesTest.cs | 1 + .../ContentType/PostJsonPatchContentTypeTest.cs | 1 + .../PostJsonPatchContentWithCharsetTypeTest.cs | 1 + .../Endpoints/Enum/GetAndReturnEnumTest.cs | 1 + .../Endpoints/HttpMethods/TestDeleteTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestGetTest.cs | 1 + .../Endpoints/HttpMethods/TestPatchTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPostTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPutTest.cs | 1 + .../GetAndReturnNestedWithOptionalFieldTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldAsListTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithDatetimeLikeStringTest.cs | 1 + .../GetAndReturnWithDocumentedUnknownTypeTest.cs | 1 + .../Endpoints/Object/GetAndReturnWithMapOfMapTest.cs | 1 + .../Object/GetAndReturnWithOptionalFieldTest.cs | 1 + .../Object/GetAndReturnWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithUnknownFieldTest.cs | 1 + .../MockServer/Endpoints/Pagination/ListItemsTest.cs | 1 + .../Params/GetWithAllowMultipleQueryTest.cs | 1 + .../Params/GetWithInlinePathAndQueryTest.cs | 1 + .../Endpoints/Params/GetWithInlinePathTest.cs | 1 + .../Endpoints/Params/GetWithPathAndQueryTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithPathTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithQueryTest.cs | 1 + .../Endpoints/Params/ModifyWithInlinePathTest.cs | 1 + .../Endpoints/Params/ModifyWithPathTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnBase64Test.cs | 1 + .../Endpoints/Primitive/GetAndReturnBoolTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDateTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDatetimeTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDoubleTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnIntTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnLongTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnStringTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnUuidTest.cs | 1 + .../Unit/MockServer/Endpoints/Put/AddTest.cs | 1 + .../Endpoints/Union/GetAndReturnUnionTest.cs | 1 + .../MockServer/Endpoints/Urls/NoEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithMixedCaseTest.cs | 1 + .../MockServer/Endpoints/Urls/WithUnderscoresTest.cs | 1 + .../PostWithObjectBodyandResponseTest.cs | 1 + .../Unit/MockServer/NoAuth/PostWithNoAuthTest.cs | 1 + .../MockServer/NoReqBody/GetWithNoRequestBodyTest.cs | 1 + .../NoReqBody/PostWithNoRequestBodyTest.cs | 1 + .../ReqWithHeaders/GetWithCustomHeaderTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Container/GetAndReturnListOfObjectsTest.cs | 1 + .../Container/GetAndReturnListOfPrimitivesTest.cs | 1 + .../Container/GetAndReturnMapOfPrimToObjectTest.cs | 1 + ...etAndReturnMapOfPrimToUndiscriminatedUnionTest.cs | 1 + .../Container/GetAndReturnMapPrimToPrimTest.cs | 1 + .../Endpoints/Container/GetAndReturnOptionalTest.cs | 1 + .../Container/GetAndReturnSetOfObjectsTest.cs | 1 + .../Container/GetAndReturnSetOfPrimitivesTest.cs | 1 + .../ContentType/PostJsonPatchContentTypeTest.cs | 1 + .../PostJsonPatchContentWithCharsetTypeTest.cs | 1 + .../Endpoints/Enum/GetAndReturnEnumTest.cs | 1 + .../Endpoints/HttpMethods/TestDeleteTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestGetTest.cs | 1 + .../Endpoints/HttpMethods/TestPatchTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPostTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPutTest.cs | 1 + .../GetAndReturnNestedWithOptionalFieldTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldAsListTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithDatetimeLikeStringTest.cs | 1 + .../GetAndReturnWithDocumentedUnknownTypeTest.cs | 1 + .../Endpoints/Object/GetAndReturnWithMapOfMapTest.cs | 1 + .../Object/GetAndReturnWithOptionalFieldTest.cs | 1 + .../Object/GetAndReturnWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithUnknownFieldTest.cs | 1 + .../MockServer/Endpoints/Pagination/ListItemsTest.cs | 1 + .../Params/GetWithAllowMultipleQueryTest.cs | 1 + .../Params/GetWithInlinePathAndQueryTest.cs | 1 + .../Endpoints/Params/GetWithInlinePathTest.cs | 1 + .../Endpoints/Params/GetWithPathAndQueryTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithPathTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithQueryTest.cs | 1 + .../Endpoints/Params/ModifyWithInlinePathTest.cs | 1 + .../Endpoints/Params/ModifyWithPathTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnBase64Test.cs | 1 + .../Endpoints/Primitive/GetAndReturnBoolTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDateTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDatetimeTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDoubleTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnIntTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnLongTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnStringTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnUuidTest.cs | 1 + .../Unit/MockServer/Endpoints/Put/AddTest.cs | 1 + .../Endpoints/Union/GetAndReturnUnionTest.cs | 1 + .../MockServer/Endpoints/Urls/NoEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithMixedCaseTest.cs | 1 + .../MockServer/Endpoints/Urls/WithUnderscoresTest.cs | 1 + .../PostWithObjectBodyandResponseTest.cs | 1 + .../Unit/MockServer/NoAuth/PostWithNoAuthTest.cs | 1 + .../MockServer/NoReqBody/GetWithNoRequestBodyTest.cs | 1 + .../NoReqBody/PostWithNoRequestBodyTest.cs | 1 + .../ReqWithHeaders/GetWithCustomHeaderTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Container/GetAndReturnListOfObjectsTest.cs | 1 + .../Container/GetAndReturnListOfPrimitivesTest.cs | 1 + .../Container/GetAndReturnMapOfPrimToObjectTest.cs | 1 + ...etAndReturnMapOfPrimToUndiscriminatedUnionTest.cs | 1 + .../Container/GetAndReturnMapPrimToPrimTest.cs | 1 + .../Endpoints/Container/GetAndReturnOptionalTest.cs | 1 + .../Container/GetAndReturnSetOfObjectsTest.cs | 1 + .../Container/GetAndReturnSetOfPrimitivesTest.cs | 1 + .../ContentType/PostJsonPatchContentTypeTest.cs | 1 + .../PostJsonPatchContentWithCharsetTypeTest.cs | 1 + .../Endpoints/Enum/GetAndReturnEnumTest.cs | 1 + .../Endpoints/HttpMethods/TestDeleteTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestGetTest.cs | 1 + .../Endpoints/HttpMethods/TestPatchTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPostTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPutTest.cs | 1 + .../GetAndReturnNestedWithOptionalFieldTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldAsListTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithDatetimeLikeStringTest.cs | 1 + .../GetAndReturnWithDocumentedUnknownTypeTest.cs | 1 + .../Endpoints/Object/GetAndReturnWithMapOfMapTest.cs | 1 + .../Object/GetAndReturnWithOptionalFieldTest.cs | 1 + .../Object/GetAndReturnWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithUnknownFieldTest.cs | 1 + .../MockServer/Endpoints/Pagination/ListItemsTest.cs | 1 + .../Params/GetWithAllowMultipleQueryTest.cs | 1 + .../Params/GetWithInlinePathAndQueryTest.cs | 1 + .../Endpoints/Params/GetWithInlinePathTest.cs | 1 + .../Endpoints/Params/GetWithPathAndQueryTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithPathTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithQueryTest.cs | 1 + .../Endpoints/Params/ModifyWithInlinePathTest.cs | 1 + .../Endpoints/Params/ModifyWithPathTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnBase64Test.cs | 1 + .../Endpoints/Primitive/GetAndReturnBoolTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDateTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDatetimeTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDoubleTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnIntTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnLongTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnStringTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnUuidTest.cs | 1 + .../Unit/MockServer/Endpoints/Put/AddTest.cs | 1 + .../Endpoints/Union/GetAndReturnUnionTest.cs | 1 + .../MockServer/Endpoints/Urls/NoEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithMixedCaseTest.cs | 1 + .../MockServer/Endpoints/Urls/WithUnderscoresTest.cs | 1 + .../PostWithObjectBodyandResponseTest.cs | 1 + .../Unit/MockServer/NoAuth/PostWithNoAuthTest.cs | 1 + .../MockServer/NoReqBody/GetWithNoRequestBodyTest.cs | 1 + .../NoReqBody/PostWithNoRequestBodyTest.cs | 1 + .../ReqWithHeaders/GetWithCustomHeaderTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Container/GetAndReturnListOfObjectsTest.cs | 1 + .../Container/GetAndReturnListOfPrimitivesTest.cs | 1 + .../Container/GetAndReturnMapOfPrimToObjectTest.cs | 1 + ...etAndReturnMapOfPrimToUndiscriminatedUnionTest.cs | 1 + .../Container/GetAndReturnMapPrimToPrimTest.cs | 1 + .../Endpoints/Container/GetAndReturnOptionalTest.cs | 1 + .../Container/GetAndReturnSetOfObjectsTest.cs | 1 + .../Container/GetAndReturnSetOfPrimitivesTest.cs | 1 + .../ContentType/PostJsonPatchContentTypeTest.cs | 1 + .../PostJsonPatchContentWithCharsetTypeTest.cs | 1 + .../Endpoints/Enum/GetAndReturnEnumTest.cs | 1 + .../Endpoints/HttpMethods/TestDeleteTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestGetTest.cs | 1 + .../Endpoints/HttpMethods/TestPatchTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPostTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPutTest.cs | 1 + .../GetAndReturnNestedWithOptionalFieldTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldAsListTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithDatetimeLikeStringTest.cs | 1 + .../GetAndReturnWithDocumentedUnknownTypeTest.cs | 1 + .../Endpoints/Object/GetAndReturnWithMapOfMapTest.cs | 1 + .../Object/GetAndReturnWithOptionalFieldTest.cs | 1 + .../Object/GetAndReturnWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithUnknownFieldTest.cs | 1 + .../MockServer/Endpoints/Pagination/ListItemsTest.cs | 1 + .../Params/GetWithAllowMultipleQueryTest.cs | 1 + .../Params/GetWithInlinePathAndQueryTest.cs | 1 + .../Endpoints/Params/GetWithInlinePathTest.cs | 1 + .../Endpoints/Params/GetWithPathAndQueryTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithPathTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithQueryTest.cs | 1 + .../Endpoints/Params/ModifyWithInlinePathTest.cs | 1 + .../Endpoints/Params/ModifyWithPathTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnBase64Test.cs | 1 + .../Endpoints/Primitive/GetAndReturnBoolTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDateTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDatetimeTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDoubleTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnIntTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnLongTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnStringTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnUuidTest.cs | 1 + .../Unit/MockServer/Endpoints/Put/AddTest.cs | 1 + .../Endpoints/Union/GetAndReturnUnionTest.cs | 1 + .../MockServer/Endpoints/Urls/NoEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithMixedCaseTest.cs | 1 + .../MockServer/Endpoints/Urls/WithUnderscoresTest.cs | 1 + .../PostWithObjectBodyandResponseTest.cs | 1 + .../Unit/MockServer/NoAuth/PostWithNoAuthTest.cs | 1 + .../MockServer/NoReqBody/GetWithNoRequestBodyTest.cs | 1 + .../NoReqBody/PostWithNoRequestBodyTest.cs | 1 + .../ReqWithHeaders/GetWithCustomHeaderTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Container/GetAndReturnListOfObjectsTest.cs | 1 + .../Container/GetAndReturnListOfPrimitivesTest.cs | 1 + .../Container/GetAndReturnMapOfPrimToObjectTest.cs | 1 + ...etAndReturnMapOfPrimToUndiscriminatedUnionTest.cs | 1 + .../Container/GetAndReturnMapPrimToPrimTest.cs | 1 + .../Endpoints/Container/GetAndReturnOptionalTest.cs | 1 + .../Container/GetAndReturnSetOfObjectsTest.cs | 1 + .../Container/GetAndReturnSetOfPrimitivesTest.cs | 1 + .../ContentType/PostJsonPatchContentTypeTest.cs | 1 + .../PostJsonPatchContentWithCharsetTypeTest.cs | 1 + .../Endpoints/Enum/GetAndReturnEnumTest.cs | 1 + .../Endpoints/HttpMethods/TestDeleteTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestGetTest.cs | 1 + .../Endpoints/HttpMethods/TestPatchTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPostTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPutTest.cs | 1 + .../GetAndReturnNestedWithOptionalFieldTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldAsListTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithDatetimeLikeStringTest.cs | 1 + .../GetAndReturnWithDocumentedUnknownTypeTest.cs | 1 + .../Endpoints/Object/GetAndReturnWithMapOfMapTest.cs | 1 + .../Object/GetAndReturnWithOptionalFieldTest.cs | 1 + .../Object/GetAndReturnWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithUnknownFieldTest.cs | 1 + .../MockServer/Endpoints/Pagination/ListItemsTest.cs | 1 + .../Params/GetWithAllowMultipleQueryTest.cs | 1 + .../Params/GetWithInlinePathAndQueryTest.cs | 1 + .../Endpoints/Params/GetWithInlinePathTest.cs | 1 + .../Endpoints/Params/GetWithPathAndQueryTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithPathTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithQueryTest.cs | 1 + .../Endpoints/Params/ModifyWithInlinePathTest.cs | 1 + .../Endpoints/Params/ModifyWithPathTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnBase64Test.cs | 1 + .../Endpoints/Primitive/GetAndReturnBoolTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDateTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDatetimeTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDoubleTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnIntTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnLongTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnStringTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnUuidTest.cs | 1 + .../Unit/MockServer/Endpoints/Put/AddTest.cs | 1 + .../Endpoints/Union/GetAndReturnUnionTest.cs | 1 + .../MockServer/Endpoints/Urls/NoEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithMixedCaseTest.cs | 1 + .../MockServer/Endpoints/Urls/WithUnderscoresTest.cs | 1 + .../PostWithObjectBodyandResponseTest.cs | 1 + .../Unit/MockServer/NoAuth/PostWithNoAuthTest.cs | 1 + .../MockServer/NoReqBody/GetWithNoRequestBodyTest.cs | 1 + .../NoReqBody/PostWithNoRequestBodyTest.cs | 1 + .../ReqWithHeaders/GetWithCustomHeaderTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Container/GetAndReturnListOfObjectsTest.cs | 1 + .../Container/GetAndReturnListOfPrimitivesTest.cs | 1 + .../Container/GetAndReturnMapOfPrimToObjectTest.cs | 1 + ...etAndReturnMapOfPrimToUndiscriminatedUnionTest.cs | 1 + .../Container/GetAndReturnMapPrimToPrimTest.cs | 1 + .../Endpoints/Container/GetAndReturnOptionalTest.cs | 1 + .../Container/GetAndReturnSetOfObjectsTest.cs | 1 + .../Container/GetAndReturnSetOfPrimitivesTest.cs | 1 + .../ContentType/PostJsonPatchContentTypeTest.cs | 1 + .../PostJsonPatchContentWithCharsetTypeTest.cs | 1 + .../Endpoints/Enum/GetAndReturnEnumTest.cs | 1 + .../Endpoints/HttpMethods/TestDeleteTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestGetTest.cs | 1 + .../Endpoints/HttpMethods/TestPatchTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPostTest.cs | 1 + .../MockServer/Endpoints/HttpMethods/TestPutTest.cs | 1 + .../GetAndReturnNestedWithOptionalFieldTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldAsListTest.cs | 1 + .../GetAndReturnNestedWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithDatetimeLikeStringTest.cs | 1 + .../GetAndReturnWithDocumentedUnknownTypeTest.cs | 1 + .../Endpoints/Object/GetAndReturnWithMapOfMapTest.cs | 1 + .../Object/GetAndReturnWithOptionalFieldTest.cs | 1 + .../Object/GetAndReturnWithRequiredFieldTest.cs | 1 + .../Object/GetAndReturnWithUnknownFieldTest.cs | 1 + .../MockServer/Endpoints/Pagination/ListItemsTest.cs | 1 + .../Params/GetWithAllowMultipleQueryTest.cs | 1 + .../Params/GetWithInlinePathAndQueryTest.cs | 1 + .../Endpoints/Params/GetWithInlinePathTest.cs | 1 + .../Endpoints/Params/GetWithPathAndQueryTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithPathTest.cs | 1 + .../MockServer/Endpoints/Params/GetWithQueryTest.cs | 1 + .../Endpoints/Params/ModifyWithInlinePathTest.cs | 1 + .../Endpoints/Params/ModifyWithPathTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnBase64Test.cs | 1 + .../Endpoints/Primitive/GetAndReturnBoolTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDateTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDatetimeTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnDoubleTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnIntTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnLongTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnStringTest.cs | 1 + .../Endpoints/Primitive/GetAndReturnUuidTest.cs | 1 + .../Unit/MockServer/Endpoints/Put/AddTest.cs | 1 + .../Endpoints/Union/GetAndReturnUnionTest.cs | 1 + .../MockServer/Endpoints/Urls/NoEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithEndingSlashTest.cs | 1 + .../MockServer/Endpoints/Urls/WithMixedCaseTest.cs | 1 + .../MockServer/Endpoints/Urls/WithUnderscoresTest.cs | 1 + .../PostWithObjectBodyandResponseTest.cs | 1 + .../Unit/MockServer/NoAuth/PostWithNoAuthTest.cs | 1 + .../MockServer/NoReqBody/GetWithNoRequestBodyTest.cs | 1 + .../NoReqBody/PostWithNoRequestBodyTest.cs | 1 + .../ReqWithHeaders/GetWithCustomHeaderTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/ExtendedInlineRequestBodyTest.cs | 1 + .../SeedExtends.Test/Unit/Serialization/DocsTest.cs | 1 + .../Unit/Serialization/ExampleTypeTest.cs | 1 + .../SeedExtends.Test/Unit/Serialization/JsonTest.cs | 1 + .../Unit/Serialization/NestedTypeTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/CreateUserTest.cs | 1 + .../Unit/Serialization/UserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/SimpleTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/SimpleTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/A/B/FooTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/A/C/FooTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../SeedApi.Test/Unit/MockServer/Folder/FooTest.cs | 1 + .../Unit/MockServer/Folder/Service/EndpointTest.cs | 1 + .../MockServer/Folder/Service/UnknownRequestTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/FooTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/Service/GetWithBearerTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/Service/GetWithBearerTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/HeadTest.cs | 1 + .../Unit/MockServer/User/ListTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 9 ++++----- .../Unit/MockServer/Payment/CreateTest.cs | 1 + .../Unit/MockServer/Payment/DeleteTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Imdb/CreateMovieTest.cs | 1 + .../Unit/MockServer/Imdb/GetMovieTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Imdb/CreateMovieTest.cs | 1 + .../Unit/MockServer/Imdb/GetMovieTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Imdb/CreateMovieTest.cs | 1 + .../Unit/MockServer/Imdb/GetMovieTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Imdb/CreateMovieTest.cs | 1 + .../Unit/MockServer/Imdb/GetMovieTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Imdb/CreateMovieTest.cs | 1 + .../Unit/MockServer/Imdb/GetMovieTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Unit/MockServer/Auth/GetTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedLicense.Test/Unit/MockServer/GetTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedLicense.Test/Unit/MockServer/GetTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Headers/SendTest.cs | 1 + .../Unit/MockServer/Inlined/SendTest.cs | 1 + .../Unit/MockServer/Path/SendTest.cs | 1 + .../Unit/MockServer/Query/SendTest.cs | 1 + .../Unit/MockServer/Reference/SendTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Headers/SendTest.cs | 1 + .../Unit/MockServer/Inlined/SendTest.cs | 1 + .../Unit/MockServer/Path/SendTest.cs | 1 + .../Unit/MockServer/Query/SendTest.cs | 1 + .../Unit/MockServer/Reference/SendTest.cs | 1 + .../Unit/Serialization/NestedUserTest.cs | 1 + .../Unit/Serialization/OrganizationTest.cs | 1 + .../Unit/Serialization/UserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Organization/CreateTest.cs | 1 + .../Unit/MockServer/User/Events/ListEventsTest.cs | 1 + .../User/Events/Metadata/GetMetadataTest.cs | 1 + .../Unit/MockServer/User/ListTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/CreateUserTest.cs | 1 + .../Unit/MockServer/User/GetUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Ec2/BootInstanceTest.cs | 1 + .../Unit/MockServer/S3/GetPresignedUrlTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Ec2/BootInstanceTest.cs | 1 + .../Unit/MockServer/S3/GetPresignedUrlTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/UploadJsonDocumentTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Dummy/GetDummyTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Retries/GetUsersTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../SeedApi.Test/Unit/MockServer/CreateTestTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/GetTestTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../NullableOptional/CreateComplexProfileTest.cs | 1 + .../MockServer/NullableOptional/CreateUserTest.cs | 1 + .../MockServer/NullableOptional/FilterByRoleTest.cs | 1 + .../NullableOptional/GetComplexProfileTest.cs | 1 + .../NullableOptional/GetNotificationSettingsTest.cs | 1 + .../NullableOptional/GetSearchResultsTest.cs | 1 + .../Unit/MockServer/NullableOptional/GetUserTest.cs | 1 + .../MockServer/NullableOptional/ListUsersTest.cs | 1 + .../MockServer/NullableOptional/SearchUsersTest.cs | 1 + .../NullableOptional/TestDeserializationTest.cs | 1 + .../NullableOptional/UpdateComplexProfileTest.cs | 1 + .../MockServer/NullableOptional/UpdateTagsTest.cs | 1 + .../MockServer/NullableOptional/UpdateUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../NullableOptional/CreateComplexProfileTest.cs | 1 + .../MockServer/NullableOptional/CreateUserTest.cs | 1 + .../MockServer/NullableOptional/FilterByRoleTest.cs | 1 + .../NullableOptional/GetComplexProfileTest.cs | 1 + .../NullableOptional/GetNotificationSettingsTest.cs | 1 + .../NullableOptional/GetSearchResultsTest.cs | 1 + .../Unit/MockServer/NullableOptional/GetUserTest.cs | 1 + .../MockServer/NullableOptional/ListUsersTest.cs | 1 + .../MockServer/NullableOptional/SearchUsersTest.cs | 1 + .../NullableOptional/TestDeserializationTest.cs | 1 + .../NullableOptional/UpdateComplexProfileTest.cs | 1 + .../MockServer/NullableOptional/UpdateTagsTest.cs | 1 + .../MockServer/NullableOptional/UpdateUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/TestGroup/TestMethodNameTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nullable/CreateUserTest.cs | 1 + .../Unit/MockServer/Nullable/DeleteUserTest.cs | 1 + .../Unit/MockServer/Nullable/GetUsersTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nullable/CreateUserTest.cs | 1 + .../Unit/MockServer/Nullable/DeleteUserTest.cs | 1 + .../Unit/MockServer/Nullable/GetUsersTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Unit/MockServer/Auth/GetTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 8 +++----- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Unit/MockServer/Auth/GetTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Unit/MockServer/Auth/GetTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Service/PostTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Nested/Api/GetSomethingTest.cs | 1 + .../MockServer/NestedNoAuth/Api/GetSomethingTest.cs | 1 + .../Unit/MockServer/Simple/GetSomethingTest.cs | 1 + .../SeedObject.Test/Unit/Serialization/NameTest.cs | 1 + .../SeedObject.Test/Unit/Serialization/TypeTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Optional/SendOptionalBodyTest.cs | 1 + ...dOptionalNullableWithAllOptionalPropertiesTest.cs | 1 + .../MockServer/Optional/SendOptionalTypedBodyTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Optional/SendOptionalBodyTest.cs | 1 + ...dOptionalNullableWithAllOptionalPropertiesTest.cs | 1 + .../MockServer/Optional/SendOptionalTypedBodyTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../SeedPackageYml.Test/Unit/MockServer/EchoTest.cs | 1 + .../Unit/MockServer/Service/NopTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Complex/SearchTest.cs | 1 + .../InlineUsers/InlineUsers/ListUsernamesTest.cs | 1 + .../InlineUsers/ListWithBodyCursorPaginationTest.cs | 1 + .../InlineUsers/ListWithBodyOffsetPaginationTest.cs | 1 + .../InlineUsers/ListWithCursorPaginationTest.cs | 1 + .../ListWithDoubleOffsetPaginationTest.cs | 1 + .../ListWithExtendedResultsAndOptionalDataTest.cs | 1 + .../InlineUsers/ListWithExtendedResultsTest.cs | 1 + .../InlineUsers/ListWithGlobalConfigTest.cs | 1 + .../ListWithMixedTypeCursorPaginationTest.cs | 1 + .../ListWithOffsetPaginationHasNextPageTest.cs | 1 + .../InlineUsers/ListWithOffsetPaginationTest.cs | 1 + .../InlineUsers/ListWithOffsetStepPaginationTest.cs | 1 + .../Unit/MockServer/Users/ListUsernamesTest.cs | 1 + .../Users/ListUsernamesWithOptionalResponseTest.cs | 1 + .../Users/ListWithBodyCursorPaginationTest.cs | 1 + .../Users/ListWithBodyOffsetPaginationTest.cs | 1 + .../MockServer/Users/ListWithCursorPaginationTest.cs | 1 + .../Users/ListWithDoubleOffsetPaginationTest.cs | 1 + .../ListWithExtendedResultsAndOptionalDataTest.cs | 1 + .../MockServer/Users/ListWithExtendedResultsTest.cs | 1 + .../MockServer/Users/ListWithGlobalConfigTest.cs | 1 + .../Users/ListWithMixedTypeCursorPaginationTest.cs | 1 + .../Users/ListWithOffsetPaginationHasNextPageTest.cs | 1 + .../MockServer/Users/ListWithOffsetPaginationTest.cs | 1 + .../Users/ListWithOffsetStepPaginationTest.cs | 1 + .../MockServer/Users/ListWithOptionalDataTest.cs | 1 + .../ListWithTopLevelBodyCursorPaginationTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Complex/SearchTest.cs | 1 + .../InlineUsers/InlineUsers/ListUsernamesTest.cs | 1 + .../InlineUsers/ListWithBodyCursorPaginationTest.cs | 1 + .../InlineUsers/ListWithBodyOffsetPaginationTest.cs | 1 + .../InlineUsers/ListWithCursorPaginationTest.cs | 1 + .../ListWithDoubleOffsetPaginationTest.cs | 1 + .../ListWithExtendedResultsAndOptionalDataTest.cs | 1 + .../InlineUsers/ListWithExtendedResultsTest.cs | 1 + .../InlineUsers/ListWithGlobalConfigTest.cs | 1 + .../ListWithMixedTypeCursorPaginationTest.cs | 1 + .../ListWithOffsetPaginationHasNextPageTest.cs | 1 + .../InlineUsers/ListWithOffsetPaginationTest.cs | 1 + .../InlineUsers/ListWithOffsetStepPaginationTest.cs | 1 + .../Unit/MockServer/Users/ListUsernamesTest.cs | 1 + .../Users/ListUsernamesWithOptionalResponseTest.cs | 1 + .../Users/ListWithBodyCursorPaginationTest.cs | 1 + .../Users/ListWithBodyOffsetPaginationTest.cs | 1 + .../MockServer/Users/ListWithCursorPaginationTest.cs | 1 + .../Users/ListWithDoubleOffsetPaginationTest.cs | 1 + .../ListWithExtendedResultsAndOptionalDataTest.cs | 1 + .../MockServer/Users/ListWithExtendedResultsTest.cs | 1 + .../MockServer/Users/ListWithGlobalConfigTest.cs | 1 + .../Users/ListWithMixedTypeCursorPaginationTest.cs | 1 + .../Users/ListWithOffsetPaginationHasNextPageTest.cs | 1 + .../MockServer/Users/ListWithOffsetPaginationTest.cs | 1 + .../Users/ListWithOffsetStepPaginationTest.cs | 1 + .../MockServer/Users/ListWithOptionalDataTest.cs | 1 + .../ListWithTopLevelBodyCursorPaginationTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Complex/SearchTest.cs | 1 + .../InlineUsers/InlineUsers/ListUsernamesTest.cs | 1 + .../InlineUsers/ListWithBodyCursorPaginationTest.cs | 1 + .../InlineUsers/ListWithBodyOffsetPaginationTest.cs | 1 + .../InlineUsers/ListWithCursorPaginationTest.cs | 1 + .../ListWithDoubleOffsetPaginationTest.cs | 1 + .../ListWithExtendedResultsAndOptionalDataTest.cs | 1 + .../InlineUsers/ListWithExtendedResultsTest.cs | 1 + .../InlineUsers/ListWithGlobalConfigTest.cs | 1 + .../ListWithMixedTypeCursorPaginationTest.cs | 1 + .../ListWithOffsetPaginationHasNextPageTest.cs | 1 + .../InlineUsers/ListWithOffsetPaginationTest.cs | 1 + .../InlineUsers/ListWithOffsetStepPaginationTest.cs | 1 + .../Unit/MockServer/Users/ListUsernamesTest.cs | 1 + .../Users/ListUsernamesWithOptionalResponseTest.cs | 1 + .../Users/ListWithBodyCursorPaginationTest.cs | 1 + .../Users/ListWithBodyOffsetPaginationTest.cs | 1 + .../MockServer/Users/ListWithCursorPaginationTest.cs | 1 + .../Users/ListWithDoubleOffsetPaginationTest.cs | 1 + .../ListWithExtendedResultsAndOptionalDataTest.cs | 1 + .../MockServer/Users/ListWithExtendedResultsTest.cs | 1 + .../MockServer/Users/ListWithGlobalConfigTest.cs | 1 + .../Users/ListWithMixedTypeCursorPaginationTest.cs | 1 + .../Users/ListWithOffsetPaginationHasNextPageTest.cs | 1 + .../MockServer/Users/ListWithOffsetPaginationTest.cs | 1 + .../Users/ListWithOffsetStepPaginationTest.cs | 1 + .../MockServer/Users/ListWithOptionalDataTest.cs | 1 + .../ListWithTopLevelBodyCursorPaginationTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/Organizations/GetOrganizationTest.cs | 1 + .../Organizations/GetOrganizationUserTest.cs | 1 + .../Organizations/SearchOrganizationsTest.cs | 1 + .../Unit/MockServer/User/CreateUserTest.cs | 1 + .../Unit/MockServer/User/GetUserMetadataTest.cs | 1 + .../Unit/MockServer/User/GetUserSpecificsTest.cs | 1 + .../Unit/MockServer/User/GetUserTest.cs | 1 + .../Unit/MockServer/User/SearchUsersTest.cs | 1 + .../Unit/MockServer/User/UpdateUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/Organizations/GetOrganizationTest.cs | 1 + .../Organizations/GetOrganizationUserTest.cs | 1 + .../Organizations/SearchOrganizationsTest.cs | 1 + .../Unit/MockServer/User/CreateUserTest.cs | 1 + .../Unit/MockServer/User/GetUserMetadataTest.cs | 1 + .../Unit/MockServer/User/GetUserSpecificsTest.cs | 1 + .../Unit/MockServer/User/GetUserTest.cs | 1 + .../Unit/MockServer/User/SearchUsersTest.cs | 1 + .../Unit/MockServer/User/UpdateUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/GetTextTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedApi.Test/Unit/MockServer/SearchTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedApi.Test/Unit/MockServer/SearchTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetUsernameTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/User/CreateUsernameOptionalTest.cs | 1 + .../Unit/MockServer/User/CreateUsernameTest.cs | 1 + .../User/CreateUsernameWithReferencedTypeTest.cs | 1 + .../Unit/MockServer/User/GetUsernameTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/User/CreateUsernameOptionalTest.cs | 1 + .../Unit/MockServer/User/CreateUsernameTest.cs | 1 + .../User/CreateUsernameWithReferencedTypeTest.cs | 1 + .../Unit/MockServer/User/GetUsernameTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedApi.Test/Unit/MockServer/GetFooTest.cs | 1 + .../SeedApi.Test/Unit/MockServer/UpdateFooTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedApi.Test/Unit/MockServer/GetFooTest.cs | 1 + .../SeedApi.Test/Unit/MockServer/UpdateFooTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Package/TestTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../src/SeedApi.Test/Unit/MockServer/GetTokenTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/GetUserTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/GetUsersTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../SeedApi.Test/Unit/MockServer/GetAccountTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Dummy/GetDummyTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Dummy/GetDummyTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Dummy/GenerateTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Dummy/GenerateTest.cs | 1 + .../MockServer/Admin/SendTestSubmissionUpdateTest.cs | 1 + .../Admin/SendWorkspaceSubmissionUpdateTest.cs | 1 + .../Unit/MockServer/Admin/StoreTracedTestCaseTest.cs | 1 + .../MockServer/Admin/StoreTracedTestCaseV2Test.cs | 1 + .../MockServer/Admin/StoreTracedWorkspaceTest.cs | 1 + .../MockServer/Admin/StoreTracedWorkspaceV2Test.cs | 1 + .../Admin/UpdateTestSubmissionStatusTest.cs | 1 + .../Admin/UpdateWorkspaceSubmissionStatusTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../MockServer/Homepage/GetHomepageProblemsTest.cs | 1 + .../MockServer/Homepage/SetHomepageProblemsTest.cs | 1 + .../Migration/GetAttemptedMigrationsTest.cs | 1 + .../Unit/MockServer/Playlist/CreatePlaylistTest.cs | 1 + .../Unit/MockServer/Playlist/DeletePlaylistTest.cs | 1 + .../Unit/MockServer/Playlist/GetPlaylistTest.cs | 1 + .../Unit/MockServer/Playlist/GetPlaylistsTest.cs | 1 + .../Unit/MockServer/Playlist/UpdatePlaylistTest.cs | 1 + .../Unit/MockServer/Problem/CreateProblemTest.cs | 1 + .../Unit/MockServer/Problem/DeleteProblemTest.cs | 1 + .../MockServer/Problem/GetDefaultStarterFilesTest.cs | 1 + .../Unit/MockServer/Problem/UpdateProblemTest.cs | 1 + .../Submission/CreateExecutionSessionTest.cs | 1 + .../MockServer/Submission/GetExecutionSessionTest.cs | 1 + .../Submission/GetExecutionSessionsStateTest.cs | 1 + .../Submission/StopExecutionSessionTest.cs | 1 + .../MockServer/Sysprop/GetNumWarmInstancesTest.cs | 1 + .../MockServer/Sysprop/SetNumWarmInstancesTest.cs | 1 + .../MockServer/V2/Problem/GetLatestProblemTest.cs | 1 + .../V2/Problem/GetLightweightProblemsTest.cs | 1 + .../MockServer/V2/Problem/GetProblemVersionTest.cs | 1 + .../Unit/MockServer/V2/Problem/GetProblemsTest.cs | 1 + .../SeedTrace.Test/Unit/MockServer/V2/TestTest.cs | 1 + .../MockServer/V2/V3/Problem/GetLatestProblemTest.cs | 1 + .../V2/V3/Problem/GetLightweightProblemsTest.cs | 1 + .../V2/V3/Problem/GetProblemVersionTest.cs | 1 + .../Unit/MockServer/V2/V3/Problem/GetProblemsTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Union/CallTest.cs | 1 + .../Unit/MockServer/Union/DuplicateTypesUnionTest.cs | 1 + .../Unit/MockServer/Union/GetMetadataTest.cs | 1 + .../Unit/MockServer/Union/GetTest.cs | 1 + .../Unit/MockServer/Union/NestedUnionsTest.cs | 1 + .../MockServer/Union/TestCamelCasePropertiesTest.cs | 1 + .../Unit/MockServer/Union/UpdateMetadataTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Union/CallTest.cs | 1 + .../Unit/MockServer/Union/DuplicateTypesUnionTest.cs | 1 + .../Unit/MockServer/Union/GetMetadataTest.cs | 1 + .../Unit/MockServer/Union/GetTest.cs | 1 + .../Unit/MockServer/Union/NestedUnionsTest.cs | 1 + .../MockServer/Union/TestCamelCasePropertiesTest.cs | 1 + .../Unit/MockServer/Union/UpdateMetadataTest.cs | 1 + .../SeedUnions.Test/Unit/Serialization/BarTest.cs | 1 + .../Unit/Serialization/FooExtendedTest.cs | 1 + .../SeedUnions.Test/Unit/Serialization/FooTest.cs | 1 + .../Unit/Serialization/GetShapeRequestTest.cs | 1 + .../Unit/Serialization/NormalSweetTest.cs | 1 + .../Unit/Serialization/ThankfulFactorTest.cs | 1 + .../Serialization/UnionWithDuplicatePrimitiveTest.cs | 1 + .../Serialization/UnionWithDuplicateTypesTest.cs | 1 + .../UnionWithMultipleNoPropertiesTest.cs | 1 + .../Unit/Serialization/UnionWithNoPropertiesTest.cs | 1 + .../Unit/Serialization/UnionWithOptionalTimeTest.cs | 1 + .../Unit/Serialization/UnionWithPrimitiveTest.cs | 1 + .../Serialization/UnionWithSameNumberTypesTest.cs | 1 + .../Serialization/UnionWithSameStringTypesTest.cs | 1 + .../Unit/Serialization/UnionWithSingleElementTest.cs | 1 + .../Unit/Serialization/UnionWithSubTypesTest.cs | 1 + .../Unit/Serialization/UnionWithTimeTest.cs | 1 + .../Unit/Serialization/UnionWithoutKeyTest.cs | 1 + .../SeedUnions.Test/Unit/Serialization/BarTest.cs | 1 + .../Unit/Serialization/FooExtendedTest.cs | 1 + .../SeedUnions.Test/Unit/Serialization/FooTest.cs | 1 + .../Unit/Serialization/GetShapeRequestTest.cs | 1 + .../Unit/Serialization/NormalSweetTest.cs | 1 + .../Unit/Serialization/ThankfulFactorTest.cs | 1 + .../Serialization/UnionWithDuplicatePrimitiveTest.cs | 1 + .../Serialization/UnionWithDuplicateTypesTest.cs | 1 + .../UnionWithMultipleNoPropertiesTest.cs | 1 + .../Unit/Serialization/UnionWithNoPropertiesTest.cs | 1 + .../Unit/Serialization/UnionWithOptionalTimeTest.cs | 1 + .../Unit/Serialization/UnionWithPrimitiveTest.cs | 1 + .../Serialization/UnionWithSameNumberTypesTest.cs | 1 + .../Serialization/UnionWithSameStringTypesTest.cs | 1 + .../Unit/Serialization/UnionWithSingleElementTest.cs | 1 + .../Unit/Serialization/UnionWithSubTypesTest.cs | 1 + .../Unit/Serialization/UnionWithTimeTest.cs | 1 + .../Unit/Serialization/UnionWithoutKeyTest.cs | 1 + .../SeedUnions.Test/Unit/Serialization/BarTest.cs | 1 + .../Unit/Serialization/FooExtendedTest.cs | 1 + .../SeedUnions.Test/Unit/Serialization/FooTest.cs | 1 + .../Unit/Serialization/GetShapeRequestTest.cs | 1 + .../Unit/Serialization/NormalSweetTest.cs | 1 + .../Unit/Serialization/ThankfulFactorTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Unknown/PostObjectTest.cs | 1 + .../Unit/MockServer/Unknown/PostTest.cs | 1 + .../Unit/Serialization/MyObjectTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/SubmitFormDataTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/CreateTest.cs | 1 + .../SeedValidation.Test/Unit/MockServer/GetTest.cs | 1 + .../Unit/Serialization/TypeTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/Service/PostTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetUserTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- .../Unit/MockServer/User/GetUserTest.cs | 1 + .../Auth/GetTokenWithClientCredentialsTest.cs | 1 + .../Unit/MockServer/Auth/RefreshTokenTest.cs | 1 + .../Unit/MockServer/BaseMockServerTest.cs | 7 +++---- 913 files changed, 1162 insertions(+), 453 deletions(-) diff --git a/generators/csharp/codegen/src/ast/types/TestClass.ts b/generators/csharp/codegen/src/ast/types/TestClass.ts index 3464a501a17c..fc34c8dd4a9f 100644 --- a/generators/csharp/codegen/src/ast/types/TestClass.ts +++ b/generators/csharp/codegen/src/ast/types/TestClass.ts @@ -2,6 +2,7 @@ import { type Generation } from "../../context/generation-info.js"; import { Node } from "../core/AstNode.js"; import { Writer } from "../core/Writer.js"; import { Access } from "../language/Access.js"; +import { Annotation } from "../language/Annotation.js"; import { CodeBlock } from "../language/CodeBlock.js"; import { Class } from "./Class.js"; import { type ClassReference } from "./ClassReference.js"; @@ -53,12 +54,19 @@ export class TestClass extends Node { } public getClass(): Class { + const annotations: (Annotation | ClassReference)[] = [ + this.NUnit.Framework.TestFixture, + this.csharp.annotation({ + reference: this.NUnit.Framework.Parallelizable, + argument: "ParallelScope.Self" + }) + ]; const _class = new Class( { access: Access.Public, name: this.name, namespace: this.namespace, - annotations: [this.NUnit.Framework.TestFixture], + annotations, parentClassReference: this.parentClassReference, origin: this.origin }, diff --git a/generators/csharp/codegen/src/context/extern.ts b/generators/csharp/codegen/src/context/extern.ts index 34edd19ad30a..717f5b337393 100644 --- a/generators/csharp/codegen/src/context/extern.ts +++ b/generators/csharp/codegen/src/context/extern.ts @@ -771,6 +771,14 @@ export class Extern { this.csharp.classReference({ name: "SetUpFixture", namespace: "NUnit.Framework" + }), + /** + * Reference to NUnit.Framework.ParallelizableAttribute class. + */ + Parallelizable: () => + this.csharp.classReference({ + name: "Parallelizable", + namespace: "NUnit.Framework" }) }) }); diff --git a/generators/csharp/sdk/src/test-generation/mock-server/BaseMockServerTestGenerator.ts b/generators/csharp/sdk/src/test-generation/mock-server/BaseMockServerTestGenerator.ts index 2e1b71b60181..b10398214645 100644 --- a/generators/csharp/sdk/src/test-generation/mock-server/BaseMockServerTestGenerator.ts +++ b/generators/csharp/sdk/src/test-generation/mock-server/BaseMockServerTestGenerator.ts @@ -28,14 +28,12 @@ export class BaseMockServerTestGenerator extends FileGenerator Date: Fri, 13 Mar 2026 15:16:33 -0400 Subject: [PATCH 06/29] feat(csharp): rework enum serialization to eliminate reflection (#13519) * feat(csharp): rework enum serialization to eliminate reflection - Regular enums now generate a companion serializer class with compile-time switch statements instead of using generic EnumSerializer - String enums now generate a nested serializer class instead of using StringEnumSerializer which relied on Activator.CreateInstance - Remove generic EnumSerializer.Template.cs and StringEnumSerializer.Template.cs from as-is includes - Update template test files to use inline serializers Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> * fix: biome formatting and version bump for feat changelog entries Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> * fix: use valueProperty.name in string enum serializer Write method to handle renamed Value property Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> * refactor: use dictionary lookups instead of switch statements for enum serializers Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> * chore: update enum seed test snapshots for dictionary-based serializers Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> * chore: add benchmark results to changelog entries Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../asIs/QueryStringConverterTest.Template.cs | 42 +++++- .../test/Json/EnumSerializerTests.Template.cs | 30 +++- .../StringEnumSerializerTests.Template.cs | 16 +- .../base/src/context/GeneratorContext.ts | 13 +- .../csharp/codegen/src/ast/types/Enum.ts | 84 ++++++++++- .../csharp/model/src/ModelGeneratorContext.ts | 8 - .../csharp/model/src/enum/EnumGenerator.ts | 30 ++-- .../model/src/enum/StringEnumGenerator.ts | 97 ++++++++++-- generators/csharp/model/versions.yml | 14 ++ .../csharp/sdk/src/SdkGeneratorContext.ts | 8 - generators/csharp/sdk/versions.yml | 14 ++ .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedEnum/Color.cs | 29 +++- .../src/SeedEnum/Core/StringEnumSerializer.cs | 25 ---- .../src/SeedEnum/EnumWithCustom.cs | 29 +++- .../src/SeedEnum/EnumWithSpecialCharacters.cs | 29 +++- .../src/SeedEnum/ForwardCompatibleEnum.cs | 29 +++- .../src/SeedEnum/Operand.cs | 29 +++- .../src/SeedEnum/SpecialEnum.cs | 29 +++- .../src/SeedEnum/Unknown/Status.cs | 29 +++- .../Core/Json/EnumSerializerTests.cs | 60 -------- .../enum/plain-enums/src/SeedEnum/Color.cs | 39 ++++- .../src/SeedEnum/Core/EnumSerializer.cs | 53 ------- .../src/SeedEnum/EnumWithCustom.cs | 48 +++++- .../src/SeedEnum/EnumWithSpecialCharacters.cs | 48 +++++- .../src/SeedEnum/ForwardCompatibleEnum.cs | 48 +++++- .../enum/plain-enums/src/SeedEnum/Operand.cs | 49 ++++++- .../plain-enums/src/SeedEnum/SpecialEnum.cs | 108 +++++++++++++- .../src/SeedEnum/Unknown/Status.cs | 39 ++++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Union/KeyType.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedEnum/Core/StringEnumSerializer.cs | 25 ---- .../src/SeedEnum/Types/Color.cs | 29 +++- .../src/SeedEnum/Types/EnumWithCustom.cs | 29 +++- .../Types/EnumWithSpecialCharacters.cs | 29 +++- .../SeedEnum/Types/ForwardCompatibleEnum.cs | 29 +++- .../src/SeedEnum/Types/Operand.cs | 29 +++- .../src/SeedEnum/Types/SpecialEnum.cs | 29 +++- .../src/SeedEnum/Unknown/Types/Status.cs | 29 +++- .../Core/Json/EnumSerializerTests.cs | 60 -------- .../src/SeedEnum/Core/EnumSerializer.cs | 53 ------- .../plain-enums/src/SeedEnum/Types/Color.cs | 43 +++++- .../src/SeedEnum/Types/EnumWithCustom.cs | 44 +++++- .../Types/EnumWithSpecialCharacters.cs | 44 +++++- .../SeedEnum/Types/ForwardCompatibleEnum.cs | 44 +++++- .../plain-enums/src/SeedEnum/Types/Operand.cs | 45 +++++- .../src/SeedEnum/Types/SpecialEnum.cs | 104 ++++++++++++- .../src/SeedEnum/Unknown/Types/Status.cs | 43 +++++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Endpoints/Put/Types/ErrorCategory.cs | 29 +++- .../Endpoints/Put/Types/ErrorCode.cs | 29 +++- .../Types/Enum/Types/WeatherReport.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Endpoints/Put/Types/ErrorCategory.cs | 29 +++- .../Endpoints/Put/Types/ErrorCode.cs | 29 +++- .../Types/Enum/Types/WeatherReport.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Endpoints/Put/Types/ErrorCategory.cs | 29 +++- .../Endpoints/Put/Types/ErrorCode.cs | 29 +++- .../Types/Enum/Types/WeatherReport.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Endpoints/Put/Types/ErrorCategory.cs | 29 +++- .../Endpoints/Put/Types/ErrorCode.cs | 29 +++- .../Types/Enum/Types/WeatherReport.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Endpoints/Put/Types/ErrorCategory.cs | 29 +++- .../Endpoints/Put/Types/ErrorCode.cs | 29 +++- .../Types/Enum/Types/WeatherReport.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Endpoints/Put/Types/ErrorCategory.cs | 29 +++- .../Endpoints/Put/Types/ErrorCode.cs | 29 +++- .../Types/Enum/Types/WeatherReport.cs | 29 +++- 80 files changed, 1949 insertions(+), 1803 deletions(-) delete mode 100644 seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs delete mode 100644 seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs delete mode 100644 seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs delete mode 100644 seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Core/StringEnumSerializer.cs diff --git a/generators/csharp/base/src/asIs/QueryStringConverterTest.Template.cs b/generators/csharp/base/src/asIs/QueryStringConverterTest.Template.cs index 2c678d517f07..87f1c6fdf248 100644 --- a/generators/csharp/base/src/asIs/QueryStringConverterTest.Template.cs +++ b/generators/csharp/base/src/asIs/QueryStringConverterTest.Template.cs @@ -138,7 +138,7 @@ public void ToDeepObject_WithArrayOfObjects_ReturnsDeepObjectNotation() } // Test helper types - defined inline to avoid dependency on generated types - [JsonConverter(typeof(EnumSerializer))] + [JsonConverter(typeof(TestEnumSerializer))] private enum TestEnum { [EnumMember(Value = "value_1")] @@ -148,7 +148,31 @@ private enum TestEnum Value2 } - [JsonConverter(typeof(StringEnumSerializer))] + private class TestEnumSerializer : JsonConverter + { + public override TestEnum Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options) + { + var stringValue = reader.GetString() ?? throw new System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + "value_1" => TestEnum.Value1, + "value_2" => TestEnum.Value2, + _ => default + }; + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, TestEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(value switch + { + TestEnum.Value1 => "value_1", + TestEnum.Value2 => "value_2", + _ => throw new System.ArgumentOutOfRangeException(nameof(value), value, null) + }); + } + } + + [JsonConverter(typeof(TestStringEnum.TestStringEnumSerializer))] [System.Serializable] private readonly record struct TestStringEnum : IStringEnum { @@ -165,6 +189,20 @@ public TestStringEnum(string value) public bool Equals(string? other) => Value.Equals(other); public override string ToString() => Value; + + internal class TestStringEnumSerializer : JsonConverter + { + public override TestStringEnum Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options) + { + var stringValue = reader.GetString() ?? throw new System.Exception("The JSON value could not be read as a string."); + return new TestStringEnum(stringValue); + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, TestStringEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + } } private class TestObject diff --git a/generators/csharp/base/src/asIs/test/Json/EnumSerializerTests.Template.cs b/generators/csharp/base/src/asIs/test/Json/EnumSerializerTests.Template.cs index 33c3ee099edf..bc8fdefbb890 100644 --- a/generators/csharp/base/src/asIs/test/Json/EnumSerializerTests.Template.cs +++ b/generators/csharp/base/src/asIs/test/Json/EnumSerializerTests.Template.cs @@ -49,7 +49,7 @@ public class DummyObject public DummyEnum EnumProperty { get; set; } } -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(DummyEnumSerializer))] public enum DummyEnum { [EnumMember(Value = "known_value1")] @@ -57,4 +57,30 @@ public enum DummyEnum [EnumMember(Value = "known_value2")] KnownValue2 -} \ No newline at end of file +} + +internal class DummyEnumSerializer : JsonConverter +{ + private static readonly Dictionary _stringToEnum = new() + { + { "known_value1", DummyEnum.KnownValue1 }, + { "known_value2", DummyEnum.KnownValue2 }, + }; + + private static readonly Dictionary _enumToString = new() + { + { DummyEnum.KnownValue1, "known_value1" }, + { DummyEnum.KnownValue2, "known_value2" }, + }; + + public override DummyEnum Read(ref System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, JsonSerializerOptions options) + { + var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, DummyEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(_enumToString.TryGetValue(value, out var stringValue) ? stringValue : null); + } +} diff --git a/generators/csharp/base/src/asIs/test/Json/StringEnumSerializerTests.Template.cs b/generators/csharp/base/src/asIs/test/Json/StringEnumSerializerTests.Template.cs index 95bade50857a..dfa9d1dc3e81 100644 --- a/generators/csharp/base/src/asIs/test/Json/StringEnumSerializerTests.Template.cs +++ b/generators/csharp/base/src/asIs/test/Json/StringEnumSerializerTests.Template.cs @@ -75,7 +75,7 @@ public class DummyObject public DummyEnum EnumProperty { get; set; } } -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(DummyEnum.DummyEnumSerializer))] public readonly record struct DummyEnum : IStringEnum { public DummyEnum(string value) @@ -135,4 +135,18 @@ public override int GetHashCode() public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); + + internal class DummyEnumSerializer : JsonConverter + { + public override DummyEnum Read(ref System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, JsonSerializerOptions options) + { + var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return new DummyEnum(stringValue); + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, DummyEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + } } diff --git a/generators/csharp/base/src/context/GeneratorContext.ts b/generators/csharp/base/src/context/GeneratorContext.ts index aaf6cca51bc5..e782b253d6b5 100644 --- a/generators/csharp/base/src/context/GeneratorContext.ts +++ b/generators/csharp/base/src/context/GeneratorContext.ts @@ -840,7 +840,6 @@ export abstract class GeneratorContext extends AbstractGeneratorContext { // types that can get used this.Types.ReadOnlyAdditionalProperties(); this.Types.JsonUtils; - this.Types.StringEnumSerializer; this.Types.IStringEnum; // start with the models @@ -866,6 +865,18 @@ export abstract class GeneratorContext extends AbstractGeneratorContext { origin: this.model.explicit(typeDeclaration, "Values"), enclosingType }); + + // Register nested serializer class reference + this.csharp.classReference({ + origin: this.model.explicit(typeDeclaration, `${enclosingType.name}Serializer`), + enclosingType + }); + } else { + // Register companion serializer class reference for regular enums + this.csharp.classReference({ + origin: this.model.explicit(typeDeclaration, `${enclosingType.name}Serializer`), + namespace: enclosingType.namespace + }); } }, object: (otd: ObjectTypeDeclaration) => { diff --git a/generators/csharp/codegen/src/ast/types/Enum.ts b/generators/csharp/codegen/src/ast/types/Enum.ts index 903d5a42fd5e..ef56fb6f15cb 100644 --- a/generators/csharp/codegen/src/ast/types/Enum.ts +++ b/generators/csharp/codegen/src/ast/types/Enum.ts @@ -27,7 +27,9 @@ export declare namespace Enum { interface _Member { /* The name of the enum field */ name: string; - /* The value of the enum field */ + /* The wire value string of the enum field */ + wireValue: string; + /* The annotation for the enum field */ value: Annotation; } } @@ -45,6 +47,8 @@ export class Enum extends Node { private annotations: Annotation[]; private fields: Enum._Member[] = []; + private generateSerializer = false; + private serializerClassReference: ClassReference | undefined; constructor({ name, namespace, access, annotations, origin }: Enum.Args, generation: Generation) { super(origin, generation); @@ -62,9 +66,14 @@ export class Enum extends Node { return this.namespace; } + public addAnnotation(annotation: Annotation): void { + this.annotations.push(annotation); + } + public addMember(field: Enum.Member): void { this.fields.push({ name: field.name, + wireValue: field.value, value: this.csharp.annotation({ reference: this.System.Runtime.Serialization.EnumMember, argument: this.csharp.codeblock((writer) => { @@ -75,6 +84,19 @@ export class Enum extends Node { }); } + /** + * Enables generation of a companion JsonConverter class that uses dictionary lookups + * instead of reflection. + */ + public enableSerializerGeneration(): ClassReference { + this.generateSerializer = true; + this.serializerClassReference = this.csharp.classReference({ + name: `${this.name}Serializer`, + namespace: this.namespace + }); + return this.serializerClassReference; + } + public write(writer: Writer): void { writer.writeLine(`namespace ${this.namespace};`); writer.newLine(); @@ -99,5 +121,65 @@ export class Enum extends Node { }); writer.writeNewLineIfLastLineNot(); writer.popScope(); + + if (this.generateSerializer) { + this.writeSerializerClass(writer); + } + } + + private writeSerializerClass(writer: Writer): void { + writer.newLine(); + writer.writeLine( + `internal class ${this.name}Serializer : global::System.Text.Json.Serialization.JsonConverter<${this.name}>` + ); + writer.pushScope(); + + // Write string-to-enum dictionary + writer.writeLine( + `private static readonly global::System.Collections.Generic.Dictionary _stringToEnum = new()` + ); + writer.pushScope(); + for (const field of this.fields) { + writer.writeLine(`{ ${JSON.stringify(field.wireValue)}, ${this.name}.${field.name} },`); + } + writer.popScope(false); + writer.writeLine(";"); + writer.newLine(); + + // Write enum-to-string dictionary + writer.writeLine( + `private static readonly global::System.Collections.Generic.Dictionary<${this.name}, string> _enumToString = new()` + ); + writer.pushScope(); + for (const field of this.fields) { + writer.writeLine(`{ ${this.name}.${field.name}, ${JSON.stringify(field.wireValue)} },`); + } + writer.popScope(false); + writer.writeLine(";"); + writer.newLine(); + + // Write Read method + writer.writeLine( + `public override ${this.name} Read(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options)` + ); + writer.pushScope(); + writer.writeLine( + `var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string.");` + ); + writer.writeLine(`return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default;`); + writer.popScope(); + writer.newLine(); + + // Write Write method + writer.writeLine( + `public override void Write(global::System.Text.Json.Utf8JsonWriter writer, ${this.name} value, global::System.Text.Json.JsonSerializerOptions options)` + ); + writer.pushScope(); + writer.writeLine( + `writer.WriteStringValue(_enumToString.TryGetValue(value, out var stringValue) ? stringValue : null);` + ); + writer.popScope(); + + writer.popScope(); } } diff --git a/generators/csharp/model/src/ModelGeneratorContext.ts b/generators/csharp/model/src/ModelGeneratorContext.ts index e4e6ee2376e5..1df1d0611b85 100644 --- a/generators/csharp/model/src/ModelGeneratorContext.ts +++ b/generators/csharp/model/src/ModelGeneratorContext.ts @@ -95,11 +95,8 @@ export class ModelGeneratorContext extends GeneratorContext { ); if (this.settings.isForwardCompatibleEnumsEnabled) { - files.push(AsIsFiles.Json.StringEnumSerializer); files.push(AsIsFiles.StringEnum); files.push(AsIsFiles.StringEnumExtensions); - } else { - files.push(AsIsFiles.Json.EnumSerializer); } const resolvedProtoAnyType = this.protobufResolver.resolveWellKnownProtobufType(WellKnownProtobufType.any()); @@ -117,11 +114,6 @@ export class ModelGeneratorContext extends GeneratorContext { AsIsFiles.Test.Json.JsonAccessAttributeTests, AsIsFiles.Test.Json.OneOfSerializerTests ]; - if (this.settings.isForwardCompatibleEnumsEnabled) { - files.push(AsIsFiles.Test.Json.StringEnumSerializerTests); - } else { - files.push(AsIsFiles.Test.Json.EnumSerializerTests); - } return files; } diff --git a/generators/csharp/model/src/enum/EnumGenerator.ts b/generators/csharp/model/src/enum/EnumGenerator.ts index 3ac9b9bf05b7..8387dc8ddd96 100644 --- a/generators/csharp/model/src/enum/EnumGenerator.ts +++ b/generators/csharp/model/src/enum/EnumGenerator.ts @@ -24,22 +24,24 @@ export class EnumGenerator extends FileGenerator { - writer.write("typeof("); - writer.writeNode(this.csharp.classReferenceInternal(this.Types.EnumSerializer)); - writer.write("<"); - writer.writeNode(this.classReference); - writer.write(">"); - writer.write(")"); - }) - }) - ] + access: ast.Access.Public }); + // Enable per-enum serializer generation (no reflection) + const serializerRef = enum_.enableSerializerGeneration(); + + // Add JsonConverter annotation pointing to the generated serializer + enum_.addAnnotation( + this.csharp.annotation({ + reference: this.System.Text.Json.Serialization.JsonConverter(), + argument: this.csharp.codeblock((writer: Writer) => { + writer.write("typeof("); + writer.writeNode(this.csharp.classReferenceInternal(serializerRef)); + writer.write(")"); + }) + }) + ); + this.enumDeclaration.values.forEach((member) => enum_.addMember({ name: member.name.name.pascalCase.safeName, value: member.name.wireValue }) ); diff --git a/generators/csharp/model/src/enum/StringEnumGenerator.ts b/generators/csharp/model/src/enum/StringEnumGenerator.ts index 37f8a30fdfc6..2f5788a22f58 100644 --- a/generators/csharp/model/src/enum/StringEnumGenerator.ts +++ b/generators/csharp/model/src/enum/StringEnumGenerator.ts @@ -1,5 +1,5 @@ import { CSharpFile, FileGenerator } from "@fern-api/csharp-base"; -import { ast } from "@fern-api/csharp-codegen"; +import { ast, Writer } from "@fern-api/csharp-codegen"; import { join, RelativeFilePath } from "@fern-api/fs-utils"; import { FernIr } from "@fern-fern/ir-sdk"; @@ -28,21 +28,25 @@ export class StringEnumGenerator extends FileGenerator { - writer.write("typeof("); - writer.writeNode( - this.csharp.classReferenceInternal(this.Types.StringEnumSerializer(this.classReference)) - ); - writer.write(")"); - }) - }); + // Create the serializer origin for the nested serializer class + const serializerOrigin = this.model.explicit(this.typeDeclaration, `${this.classReference.name}Serializer`); const stringEnum = this.csharp.class_({ reference: this.classReference, interfaceReferences: [this.Types.IStringEnum], - annotations: [serializerAnnotation, this.System.Serializable], + annotations: [ + this.csharp.annotation({ + reference: this.System.Text.Json.Serialization.JsonConverter(), + argument: this.csharp.codeblock((writer: Writer) => { + // Use the same pattern as UnionGenerator: write enclosing type + ".NestedClassName" + writer.write("typeof("); + writer.writeNode(this.classReference); + writer.write(`.${this.classReference.name}Serializer`); + writer.write(")"); + }) + }), + this.System.Serializable + ], access: ast.Access.Public, type: ast.Class.ClassType.RecordStruct, readonly: true @@ -220,6 +224,75 @@ export class StringEnumGenerator extends FileGenerator`, + namespace: "System.Text.Json.Serialization" + }) + }); + + serializerClass.addMethod({ + access: ast.Access.Public, + name: "Read", + return_: this.classReference, + override: true, + type: ast.MethodType.INSTANCE, + parameters: [ + this.csharp.parameter({ + name: "reader", + type: this.csharp.classReference({ name: "Utf8JsonReader", namespace: "System.Text.Json" }), + ref: true + }), + this.csharp.parameter({ + name: "typeToConvert", + type: this.csharp.classReference({ name: "Type", namespace: "System" }) + }), + this.csharp.parameter({ + name: "options", + type: this.csharp.classReference({ name: "JsonSerializerOptions", namespace: "System.Text.Json" }) + }) + ], + body: this.csharp.codeblock((writer) => { + writer.writeLine( + `var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string.");` + ); + writer.writeTextStatement(`return new ${this.classReference.name}(stringValue)`); + }) + }); + + serializerClass.addMethod({ + access: ast.Access.Public, + name: "Write", + override: true, + type: ast.MethodType.INSTANCE, + parameters: [ + this.csharp.parameter({ + name: "writer", + type: this.csharp.classReference({ name: "Utf8JsonWriter", namespace: "System.Text.Json" }) + }), + this.csharp.parameter({ + name: "value", + type: this.classReference + }), + this.csharp.parameter({ + name: "options", + type: this.csharp.classReference({ name: "JsonSerializerOptions", namespace: "System.Text.Json" }) + }) + ], + body: this.csharp.codeblock((writer) => { + writer.writeTextStatement(`writer.WriteStringValue(value.${valueProperty.name})`); + }) + }); + + stringEnum.addNestedClass(serializerClass); + return new CSharpFile({ clazz: stringEnum, directory: this.context.getDirectoryForTypeId(this.typeDeclaration.name.typeId), diff --git a/generators/csharp/model/versions.yml b/generators/csharp/model/versions.yml index 280852568d6c..f16bc8930f67 100644 --- a/generators/csharp/model/versions.yml +++ b/generators/csharp/model/versions.yml @@ -1,4 +1,18 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 0.3.0 + changelogEntry: + - summary: | + Rework enum JSON serialization to eliminate reflection. Each enum now gets + a generated serializer with static dictionary lookups instead of the generic + `EnumSerializer` / `StringEnumSerializer` that used `Enum.GetValues`, + `GetField`, `GetCustomAttributes`, and `Activator.CreateInstance` at runtime. + Serializer initialization is ~1,000x faster (18 ns vs 18 us) and allocates + 160x less memory; steady-state throughput is unchanged. The generated code is + also NativeAOT and IL-trimming compatible. + type: feat + createdAt: "2026-03-13" + irVersion: 65 + - version: 0.2.0 changelogEntry: - summary: | diff --git a/generators/csharp/sdk/src/SdkGeneratorContext.ts b/generators/csharp/sdk/src/SdkGeneratorContext.ts index 9e6f19ebc99d..ba412e0a1123 100644 --- a/generators/csharp/sdk/src/SdkGeneratorContext.ts +++ b/generators/csharp/sdk/src/SdkGeneratorContext.ts @@ -203,9 +203,6 @@ export class SdkGeneratorContext extends GeneratorContext { if (this.settings.isForwardCompatibleEnumsEnabled) { files.push(AsIsFiles.StringEnum); files.push(AsIsFiles.StringEnumExtensions); - files.push(AsIsFiles.Json.StringEnumSerializer); - } else { - files.push(AsIsFiles.Json.EnumSerializer); } const resolvedProtoAnyType = this.protobufResolver.resolveWellKnownProtobufType( FernIr.WellKnownProtobufType.any() @@ -237,11 +234,6 @@ export class SdkGeneratorContext extends GeneratorContext { files.push(AsIsFiles.Test.RawClientTests.IdempotentHeadersTests); } files.push(AsIsFiles.Test.Json.AdditionalPropertiesTests); - if (this.settings.isForwardCompatibleEnumsEnabled) { - files.push(AsIsFiles.Test.Json.StringEnumSerializerTests); - } else { - files.push(AsIsFiles.Test.Json.EnumSerializerTests); - } if (this.hasPagination()) { AsIsFiles.Test.Pagination.forEach((file) => files.push(file)); } diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 02009d9c6de5..c142b3fa1ba4 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,18 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.29.0 + changelogEntry: + - summary: | + Rework enum JSON serialization to eliminate reflection. Each enum now gets + a generated serializer with static dictionary lookups instead of the generic + `EnumSerializer` / `StringEnumSerializer` that used `Enum.GetValues`, + `GetField`, `GetCustomAttributes`, and `Activator.CreateInstance` at runtime. + Serializer initialization is ~1,000x faster (18 ns vs 18 us) and allocates + 160x less memory; steady-state throughput is unchanged. The generated code is + also NativeAOT and IL-trimming compatible. + type: feat + createdAt: "2026-03-13" + irVersion: 65 + - version: 2.28.0 changelogEntry: - summary: | diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e44bc8a94105..000000000000 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEnum.Core; - -namespace SeedEnum.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Color.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Color.cs index 00ed78cb417e..f24948b80eaa 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Color.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Color.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Color.ColorSerializer))] [Serializable] public readonly record struct Color : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Color(string value) => new(value); + internal class ColorSerializer : JsonConverter + { + public override Color Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Color(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Color value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs deleted file mode 100644 index fc035970fc6c..000000000000 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEnum.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithCustom.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithCustom.cs index 2f9bce263f63..f214507e5cbf 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithCustom.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithCustom.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(EnumWithCustom.EnumWithCustomSerializer))] [Serializable] public readonly record struct EnumWithCustom : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator EnumWithCustom(string value) => new(value); + internal class EnumWithCustomSerializer : JsonConverter + { + public override EnumWithCustom Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new EnumWithCustom(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + EnumWithCustom value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithSpecialCharacters.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithSpecialCharacters.cs index 38efdbd307ec..52723f787725 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithSpecialCharacters.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/EnumWithSpecialCharacters.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(EnumWithSpecialCharacters.EnumWithSpecialCharactersSerializer))] [Serializable] public readonly record struct EnumWithSpecialCharacters : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator EnumWithSpecialCharacters(string value) => new(value); + internal class EnumWithSpecialCharactersSerializer : JsonConverter + { + public override EnumWithSpecialCharacters Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new EnumWithSpecialCharacters(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + EnumWithSpecialCharacters value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/ForwardCompatibleEnum.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/ForwardCompatibleEnum.cs index 4d5276b6c357..52782995ec29 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/ForwardCompatibleEnum.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/ForwardCompatibleEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ForwardCompatibleEnum.ForwardCompatibleEnumSerializer))] [Serializable] public readonly record struct ForwardCompatibleEnum : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator ForwardCompatibleEnum(string value) => new(value); + internal class ForwardCompatibleEnumSerializer : JsonConverter + { + public override ForwardCompatibleEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ForwardCompatibleEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ForwardCompatibleEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Operand.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Operand.cs index 87dd029b443a..de08ac473fbb 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Operand.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Operand.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Operand.OperandSerializer))] [Serializable] public readonly record struct Operand : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator Operand(string value) => new(value); + internal class OperandSerializer : JsonConverter + { + public override Operand Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Operand(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Operand value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/SpecialEnum.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/SpecialEnum.cs index 70be81361dc4..61cbf8dd42de 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/SpecialEnum.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/SpecialEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(SpecialEnum.SpecialEnumSerializer))] [Serializable] public readonly record struct SpecialEnum : IStringEnum { @@ -112,6 +113,32 @@ public override string ToString() public static explicit operator SpecialEnum(string value) => new(value); + internal class SpecialEnumSerializer : JsonConverter + { + public override SpecialEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SpecialEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SpecialEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Unknown/Status.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Unknown/Status.cs index 424ae29ee31a..66ae095c03f7 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Unknown/Status.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Unknown/Status.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Status.StatusSerializer))] [Serializable] public readonly record struct Status : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Status(string value) => new(value); + internal class StatusSerializer : JsonConverter + { + public override Status Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Status(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Status value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs deleted file mode 100644 index 55c5d056247d..000000000000 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using global::System.Runtime.Serialization; -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEnum.Core; - -namespace SeedEnum.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private const DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private const string KnownEnumValue2String = "known_value2"; - - private const string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2String}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2String)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(EnumSerializer))] -public enum DummyEnum -{ - [EnumMember(Value = "known_value1")] - KnownValue1, - - [EnumMember(Value = "known_value2")] - KnownValue2, -} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Color.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Color.cs index 754f0effaedb..8bd7be58e81b 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Color.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Color.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(ColorSerializer))] public enum Color { [EnumMember(Value = "red")] @@ -13,3 +12,39 @@ public enum Color [EnumMember(Value = "blue")] Blue, } + +internal class ColorSerializer : global::System.Text.Json.Serialization.JsonConverter +{ + private static readonly global::System.Collections.Generic.Dictionary< + string, + Color + > _stringToEnum = new() { { "red", Color.Red }, { "blue", Color.Blue } }; + + private static readonly global::System.Collections.Generic.Dictionary< + Color, + string + > _enumToString = new() { { Color.Red, "red" }, { Color.Blue, "blue" } }; + + public override Color Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + Color value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null + ); + } +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs deleted file mode 100644 index f0a3a4c2719c..000000000000 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs +++ /dev/null @@ -1,53 +0,0 @@ -using global::System.Runtime.Serialization; -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEnum.Core; - -internal class EnumSerializer : JsonConverter - where TEnum : struct, Enum -{ - private readonly Dictionary _enumToString = new(); - private readonly Dictionary _stringToEnum = new(); - - public EnumSerializer() - { - var type = typeof(TEnum); - var values = Enum.GetValues(type); - - foreach (var value in values) - { - var enumValue = (TEnum)value; - var enumMember = type.GetField(enumValue.ToString())!; - var attr = enumMember - .GetCustomAttributes(typeof(EnumMemberAttribute), false) - .Cast() - .FirstOrDefault(); - - var stringValue = - attr?.Value - ?? value.ToString() - ?? throw new global::System.Exception("Unexpected null enum toString value"); - - _enumToString.Add(enumValue, stringValue); - _stringToEnum.Add(stringValue, enumValue); - } - } - - public override TEnum Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; - } - - public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) - { - writer.WriteStringValue(_enumToString[value]); - } -} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithCustom.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithCustom.cs index 54bfe7c30ae1..1250d49d8a1a 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithCustom.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithCustom.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(EnumWithCustomSerializer))] public enum EnumWithCustom { [EnumMember(Value = "safe")] @@ -13,3 +12,48 @@ public enum EnumWithCustom [EnumMember(Value = "Custom")] Custom, } + +internal class EnumWithCustomSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + private static readonly global::System.Collections.Generic.Dictionary< + string, + EnumWithCustom + > _stringToEnum = new() + { + { "safe", EnumWithCustom.Safe }, + { "Custom", EnumWithCustom.Custom }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + EnumWithCustom, + string + > _enumToString = new() + { + { EnumWithCustom.Safe, "safe" }, + { EnumWithCustom.Custom, "Custom" }, + }; + + public override EnumWithCustom Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + EnumWithCustom value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null + ); + } +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithSpecialCharacters.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithSpecialCharacters.cs index 5f5ac86caa85..7825d88d953d 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithSpecialCharacters.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/EnumWithSpecialCharacters.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(EnumWithSpecialCharactersSerializer))] public enum EnumWithSpecialCharacters { [EnumMember(Value = "\\$bla")] @@ -13,3 +12,48 @@ public enum EnumWithSpecialCharacters [EnumMember(Value = "\\$yo")] Yo, } + +internal class EnumWithSpecialCharactersSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + private static readonly global::System.Collections.Generic.Dictionary< + string, + EnumWithSpecialCharacters + > _stringToEnum = new() + { + { "\\$bla", EnumWithSpecialCharacters.Bla }, + { "\\$yo", EnumWithSpecialCharacters.Yo }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + EnumWithSpecialCharacters, + string + > _enumToString = new() + { + { EnumWithSpecialCharacters.Bla, "\\$bla" }, + { EnumWithSpecialCharacters.Yo, "\\$yo" }, + }; + + public override EnumWithSpecialCharacters Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + EnumWithSpecialCharacters value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null + ); + } +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/ForwardCompatibleEnum.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/ForwardCompatibleEnum.cs index 924c7272f353..a35a4625674f 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/ForwardCompatibleEnum.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/ForwardCompatibleEnum.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(ForwardCompatibleEnumSerializer))] public enum ForwardCompatibleEnum { [EnumMember(Value = "active")] @@ -13,3 +12,48 @@ public enum ForwardCompatibleEnum [EnumMember(Value = "inactive")] Inactive, } + +internal class ForwardCompatibleEnumSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + private static readonly global::System.Collections.Generic.Dictionary< + string, + ForwardCompatibleEnum + > _stringToEnum = new() + { + { "active", ForwardCompatibleEnum.Active }, + { "inactive", ForwardCompatibleEnum.Inactive }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + ForwardCompatibleEnum, + string + > _enumToString = new() + { + { ForwardCompatibleEnum.Active, "active" }, + { ForwardCompatibleEnum.Inactive, "inactive" }, + }; + + public override ForwardCompatibleEnum Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + ForwardCompatibleEnum value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null + ); + } +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Operand.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Operand.cs index f4f5e3d67700..57936aa1fdb7 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Operand.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Operand.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(OperandSerializer))] public enum Operand { [EnumMember(Value = ">")] @@ -16,3 +15,49 @@ public enum Operand [EnumMember(Value = "less_than")] LessThan, } + +internal class OperandSerializer : global::System.Text.Json.Serialization.JsonConverter +{ + private static readonly global::System.Collections.Generic.Dictionary< + string, + Operand + > _stringToEnum = new() + { + { ">", Operand.GreaterThan }, + { "=", Operand.EqualTo }, + { "less_than", Operand.LessThan }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + Operand, + string + > _enumToString = new() + { + { Operand.GreaterThan, ">" }, + { Operand.EqualTo, "=" }, + { Operand.LessThan, "less_than" }, + }; + + public override Operand Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + Operand value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null + ); + } +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/SpecialEnum.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/SpecialEnum.cs index 1ef9e8f2e2fd..d19e6d25361b 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/SpecialEnum.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/SpecialEnum.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(SpecialEnumSerializer))] public enum SpecialEnum { [EnumMember(Value = "")] @@ -103,3 +102,108 @@ public enum SpecialEnum [EnumMember(Value = "transcript[transcriptType='final']")] Gg, } + +internal class SpecialEnumSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + private static readonly global::System.Collections.Generic.Dictionary< + string, + SpecialEnum + > _stringToEnum = new() + { + { "", SpecialEnum.A }, + { "Hello \\\"World\\\"", SpecialEnum.B }, + { "Hello 'World'", SpecialEnum.C }, + { "Hello\\\\World", SpecialEnum.D }, + { "Hello\\nWorld", SpecialEnum.E }, + { "Hello\\rWorld", SpecialEnum.F }, + { "Hello\\tWorld", SpecialEnum.H }, + { "Hello\\fWorld", SpecialEnum.I }, + { "Hello\\u0008World", SpecialEnum.J }, + { "Hello\\vWorld", SpecialEnum.K }, + { "Hello\\x00World", SpecialEnum.L }, + { "Hello\\u0007World", SpecialEnum.M }, + { "Hello\\u0001World", SpecialEnum.N }, + { "Hello\\u0002World", SpecialEnum.O }, + { "Hello\\u001FWorld", SpecialEnum.P }, + { "Hello\\u007FWorld", SpecialEnum.Q }, + { "Hello\\u009FWorld", SpecialEnum.R }, + { "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null", SpecialEnum.S }, + { "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007", SpecialEnum.T }, + { "Hello 世界", SpecialEnum.U }, + { "café", SpecialEnum.V }, + { "🚀", SpecialEnum.W }, + { "\\\\n", SpecialEnum.X }, + { "\\\\", SpecialEnum.Y }, + { "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}", SpecialEnum.Z }, + { "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'", SpecialEnum.Aa }, + { "C:\\\\Users\\\\John\\\\Documents\\\\file.txt", SpecialEnum.Bb }, + { "/usr/local/bin/app", SpecialEnum.Cc }, + { "\\\\d{3}-\\\\d{2}-\\\\d{4}", SpecialEnum.Dd }, + { "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}", SpecialEnum.Ee }, + { "transcript[transcriptType=\"final\"]", SpecialEnum.Ff }, + { "transcript[transcriptType='final']", SpecialEnum.Gg }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + SpecialEnum, + string + > _enumToString = new() + { + { SpecialEnum.A, "" }, + { SpecialEnum.B, "Hello \\\"World\\\"" }, + { SpecialEnum.C, "Hello 'World'" }, + { SpecialEnum.D, "Hello\\\\World" }, + { SpecialEnum.E, "Hello\\nWorld" }, + { SpecialEnum.F, "Hello\\rWorld" }, + { SpecialEnum.H, "Hello\\tWorld" }, + { SpecialEnum.I, "Hello\\fWorld" }, + { SpecialEnum.J, "Hello\\u0008World" }, + { SpecialEnum.K, "Hello\\vWorld" }, + { SpecialEnum.L, "Hello\\x00World" }, + { SpecialEnum.M, "Hello\\u0007World" }, + { SpecialEnum.N, "Hello\\u0001World" }, + { SpecialEnum.O, "Hello\\u0002World" }, + { SpecialEnum.P, "Hello\\u001FWorld" }, + { SpecialEnum.Q, "Hello\\u007FWorld" }, + { SpecialEnum.R, "Hello\\u009FWorld" }, + { SpecialEnum.S, "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null" }, + { SpecialEnum.T, "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007" }, + { SpecialEnum.U, "Hello 世界" }, + { SpecialEnum.V, "café" }, + { SpecialEnum.W, "🚀" }, + { SpecialEnum.X, "\\\\n" }, + { SpecialEnum.Y, "\\\\" }, + { SpecialEnum.Z, "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}" }, + { SpecialEnum.Aa, "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'" }, + { SpecialEnum.Bb, "C:\\\\Users\\\\John\\\\Documents\\\\file.txt" }, + { SpecialEnum.Cc, "/usr/local/bin/app" }, + { SpecialEnum.Dd, "\\\\d{3}-\\\\d{2}-\\\\d{4}" }, + { SpecialEnum.Ee, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}" }, + { SpecialEnum.Ff, "transcript[transcriptType=\"final\"]" }, + { SpecialEnum.Gg, "transcript[transcriptType='final']" }, + }; + + public override SpecialEnum Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + SpecialEnum value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null + ); + } +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Unknown/Status.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Unknown/Status.cs index a02c6320f5a8..2927f5d84b99 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Unknown/Status.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Unknown/Status.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(StatusSerializer))] public enum Status { [EnumMember(Value = "Known")] @@ -13,3 +12,39 @@ public enum Status [EnumMember(Value = "Unknown")] Unknown, } + +internal class StatusSerializer : global::System.Text.Json.Serialization.JsonConverter +{ + private static readonly global::System.Collections.Generic.Dictionary< + string, + Status + > _stringToEnum = new() { { "Known", Status.Known }, { "Unknown", Status.Unknown } }; + + private static readonly global::System.Collections.Generic.Dictionary< + Status, + string + > _enumToString = new() { { Status.Known, "Known" }, { Status.Unknown, "Unknown" } }; + + public override Status Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + Status value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null + ); + } +} diff --git a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a93332581101..000000000000 --- a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUndiscriminatedUnions.Core; - -namespace SeedUndiscriminatedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index 10fa4bd8e4eb..000000000000 --- a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUndiscriminatedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/KeyType.cs b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/KeyType.cs index 8aeadd98a54a..11b45673a238 100644 --- a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/KeyType.cs +++ b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/KeyType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedUndiscriminatedUnions.Core; namespace SeedUndiscriminatedUnions; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(KeyType.KeyTypeSerializer))] [Serializable] public readonly record struct KeyType : IStringEnum { @@ -55,6 +56,32 @@ public override string ToString() public static explicit operator KeyType(string value) => new(value); + internal class KeyTypeSerializer : JsonConverter + { + public override KeyType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new KeyType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + KeyType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value_); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e44bc8a94105..000000000000 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEnum.Core; - -namespace SeedEnum.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs deleted file mode 100644 index fc035970fc6c..000000000000 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEnum.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Color.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Color.cs index 00ed78cb417e..f24948b80eaa 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Color.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Color.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Color.ColorSerializer))] [Serializable] public readonly record struct Color : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Color(string value) => new(value); + internal class ColorSerializer : JsonConverter + { + public override Color Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Color(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Color value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithCustom.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithCustom.cs index 2f9bce263f63..f214507e5cbf 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithCustom.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithCustom.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(EnumWithCustom.EnumWithCustomSerializer))] [Serializable] public readonly record struct EnumWithCustom : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator EnumWithCustom(string value) => new(value); + internal class EnumWithCustomSerializer : JsonConverter + { + public override EnumWithCustom Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new EnumWithCustom(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + EnumWithCustom value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs index 38efdbd307ec..52723f787725 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(EnumWithSpecialCharacters.EnumWithSpecialCharactersSerializer))] [Serializable] public readonly record struct EnumWithSpecialCharacters : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator EnumWithSpecialCharacters(string value) => new(value); + internal class EnumWithSpecialCharactersSerializer : JsonConverter + { + public override EnumWithSpecialCharacters Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new EnumWithSpecialCharacters(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + EnumWithSpecialCharacters value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs index 4d5276b6c357..52782995ec29 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ForwardCompatibleEnum.ForwardCompatibleEnumSerializer))] [Serializable] public readonly record struct ForwardCompatibleEnum : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator ForwardCompatibleEnum(string value) => new(value); + internal class ForwardCompatibleEnumSerializer : JsonConverter + { + public override ForwardCompatibleEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ForwardCompatibleEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ForwardCompatibleEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Operand.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Operand.cs index 87dd029b443a..de08ac473fbb 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Operand.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/Operand.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Operand.OperandSerializer))] [Serializable] public readonly record struct Operand : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator Operand(string value) => new(value); + internal class OperandSerializer : JsonConverter + { + public override Operand Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Operand(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Operand value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/SpecialEnum.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/SpecialEnum.cs index 70be81361dc4..61cbf8dd42de 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/SpecialEnum.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Types/SpecialEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(SpecialEnum.SpecialEnumSerializer))] [Serializable] public readonly record struct SpecialEnum : IStringEnum { @@ -112,6 +113,32 @@ public override string ToString() public static explicit operator SpecialEnum(string value) => new(value); + internal class SpecialEnumSerializer : JsonConverter + { + public override SpecialEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SpecialEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SpecialEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Unknown/Types/Status.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Unknown/Types/Status.cs index 424ae29ee31a..66ae095c03f7 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Unknown/Types/Status.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Unknown/Types/Status.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Status.StatusSerializer))] [Serializable] public readonly record struct Status : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Status(string value) => new(value); + internal class StatusSerializer : JsonConverter + { + public override Status Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Status(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Status value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs deleted file mode 100644 index 55c5d056247d..000000000000 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/EnumSerializerTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using global::System.Runtime.Serialization; -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEnum.Core; - -namespace SeedEnum.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private const DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private const string KnownEnumValue2String = "known_value2"; - - private const string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2String}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2String)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(EnumSerializer))] -public enum DummyEnum -{ - [EnumMember(Value = "known_value1")] - KnownValue1, - - [EnumMember(Value = "known_value2")] - KnownValue2, -} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs deleted file mode 100644 index f0a3a4c2719c..000000000000 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/EnumSerializer.cs +++ /dev/null @@ -1,53 +0,0 @@ -using global::System.Runtime.Serialization; -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEnum.Core; - -internal class EnumSerializer : JsonConverter - where TEnum : struct, Enum -{ - private readonly Dictionary _enumToString = new(); - private readonly Dictionary _stringToEnum = new(); - - public EnumSerializer() - { - var type = typeof(TEnum); - var values = Enum.GetValues(type); - - foreach (var value in values) - { - var enumValue = (TEnum)value; - var enumMember = type.GetField(enumValue.ToString())!; - var attr = enumMember - .GetCustomAttributes(typeof(EnumMemberAttribute), false) - .Cast() - .FirstOrDefault(); - - var stringValue = - attr?.Value - ?? value.ToString() - ?? throw new global::System.Exception("Unexpected null enum toString value"); - - _enumToString.Add(enumValue, stringValue); - _stringToEnum.Add(stringValue, enumValue); - } - } - - public override TEnum Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; - } - - public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) - { - writer.WriteStringValue(_enumToString[value]); - } -} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs index 754f0effaedb..163c8232b435 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(ColorSerializer))] public enum Color { [EnumMember(Value = "red")] @@ -13,3 +12,43 @@ public enum Color [EnumMember(Value = "blue")] Blue, } + +internal class ColorSerializer : global::System.Text.Json.Serialization.JsonConverter +{ + public override Color Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + "red" => Color.Red, + "blue" => Color.Blue, + _ => default, + }; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + Color value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + value switch + { + Color.Red => "red", + Color.Blue => "blue", + _ => throw new global::System.ArgumentOutOfRangeException( + nameof(value), + value, + null + ), + } + ); + } +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs index 54bfe7c30ae1..f026f9e20285 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(EnumWithCustomSerializer))] public enum EnumWithCustom { [EnumMember(Value = "safe")] @@ -13,3 +12,44 @@ public enum EnumWithCustom [EnumMember(Value = "Custom")] Custom, } + +internal class EnumWithCustomSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + public override EnumWithCustom Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + "safe" => EnumWithCustom.Safe, + "Custom" => EnumWithCustom.Custom, + _ => default, + }; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + EnumWithCustom value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + value switch + { + EnumWithCustom.Safe => "safe", + EnumWithCustom.Custom => "Custom", + _ => throw new global::System.ArgumentOutOfRangeException( + nameof(value), + value, + null + ), + } + ); + } +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs index 5f5ac86caa85..776268be6844 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(EnumWithSpecialCharactersSerializer))] public enum EnumWithSpecialCharacters { [EnumMember(Value = "\\$bla")] @@ -13,3 +12,44 @@ public enum EnumWithSpecialCharacters [EnumMember(Value = "\\$yo")] Yo, } + +internal class EnumWithSpecialCharactersSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + public override EnumWithSpecialCharacters Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + "\\$bla" => EnumWithSpecialCharacters.Bla, + "\\$yo" => EnumWithSpecialCharacters.Yo, + _ => default, + }; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + EnumWithSpecialCharacters value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + value switch + { + EnumWithSpecialCharacters.Bla => "\\$bla", + EnumWithSpecialCharacters.Yo => "\\$yo", + _ => throw new global::System.ArgumentOutOfRangeException( + nameof(value), + value, + null + ), + } + ); + } +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs index 924c7272f353..66e4dd179dee 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(ForwardCompatibleEnumSerializer))] public enum ForwardCompatibleEnum { [EnumMember(Value = "active")] @@ -13,3 +12,44 @@ public enum ForwardCompatibleEnum [EnumMember(Value = "inactive")] Inactive, } + +internal class ForwardCompatibleEnumSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + public override ForwardCompatibleEnum Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + "active" => ForwardCompatibleEnum.Active, + "inactive" => ForwardCompatibleEnum.Inactive, + _ => default, + }; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + ForwardCompatibleEnum value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + value switch + { + ForwardCompatibleEnum.Active => "active", + ForwardCompatibleEnum.Inactive => "inactive", + _ => throw new global::System.ArgumentOutOfRangeException( + nameof(value), + value, + null + ), + } + ); + } +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs index f4f5e3d67700..ef1be49f33cf 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(OperandSerializer))] public enum Operand { [EnumMember(Value = ">")] @@ -16,3 +15,45 @@ public enum Operand [EnumMember(Value = "less_than")] LessThan, } + +internal class OperandSerializer : global::System.Text.Json.Serialization.JsonConverter +{ + public override Operand Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + ">" => Operand.GreaterThan, + "=" => Operand.EqualTo, + "less_than" => Operand.LessThan, + _ => default, + }; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + Operand value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + value switch + { + Operand.GreaterThan => ">", + Operand.EqualTo => "=", + Operand.LessThan => "less_than", + _ => throw new global::System.ArgumentOutOfRangeException( + nameof(value), + value, + null + ), + } + ); + } +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs index 1ef9e8f2e2fd..c99561192bd9 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(SpecialEnumSerializer))] public enum SpecialEnum { [EnumMember(Value = "")] @@ -103,3 +102,104 @@ public enum SpecialEnum [EnumMember(Value = "transcript[transcriptType='final']")] Gg, } + +internal class SpecialEnumSerializer + : global::System.Text.Json.Serialization.JsonConverter +{ + public override SpecialEnum Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + "" => SpecialEnum.A, + "Hello \\\"World\\\"" => SpecialEnum.B, + "Hello 'World'" => SpecialEnum.C, + "Hello\\\\World" => SpecialEnum.D, + "Hello\\nWorld" => SpecialEnum.E, + "Hello\\rWorld" => SpecialEnum.F, + "Hello\\tWorld" => SpecialEnum.H, + "Hello\\fWorld" => SpecialEnum.I, + "Hello\\u0008World" => SpecialEnum.J, + "Hello\\vWorld" => SpecialEnum.K, + "Hello\\x00World" => SpecialEnum.L, + "Hello\\u0007World" => SpecialEnum.M, + "Hello\\u0001World" => SpecialEnum.N, + "Hello\\u0002World" => SpecialEnum.O, + "Hello\\u001FWorld" => SpecialEnum.P, + "Hello\\u007FWorld" => SpecialEnum.Q, + "Hello\\u009FWorld" => SpecialEnum.R, + "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null" => SpecialEnum.S, + "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007" => SpecialEnum.T, + "Hello 世界" => SpecialEnum.U, + "café" => SpecialEnum.V, + "🚀" => SpecialEnum.W, + "\\\\n" => SpecialEnum.X, + "\\\\" => SpecialEnum.Y, + "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}" => SpecialEnum.Z, + "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'" => SpecialEnum.Aa, + "C:\\\\Users\\\\John\\\\Documents\\\\file.txt" => SpecialEnum.Bb, + "/usr/local/bin/app" => SpecialEnum.Cc, + "\\\\d{3}-\\\\d{2}-\\\\d{4}" => SpecialEnum.Dd, + "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}" => SpecialEnum.Ee, + "transcript[transcriptType=\"final\"]" => SpecialEnum.Ff, + "transcript[transcriptType='final']" => SpecialEnum.Gg, + _ => default, + }; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + SpecialEnum value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + value switch + { + SpecialEnum.A => "", + SpecialEnum.B => "Hello \\\"World\\\"", + SpecialEnum.C => "Hello 'World'", + SpecialEnum.D => "Hello\\\\World", + SpecialEnum.E => "Hello\\nWorld", + SpecialEnum.F => "Hello\\rWorld", + SpecialEnum.H => "Hello\\tWorld", + SpecialEnum.I => "Hello\\fWorld", + SpecialEnum.J => "Hello\\u0008World", + SpecialEnum.K => "Hello\\vWorld", + SpecialEnum.L => "Hello\\x00World", + SpecialEnum.M => "Hello\\u0007World", + SpecialEnum.N => "Hello\\u0001World", + SpecialEnum.O => "Hello\\u0002World", + SpecialEnum.P => "Hello\\u001FWorld", + SpecialEnum.Q => "Hello\\u007FWorld", + SpecialEnum.R => "Hello\\u009FWorld", + SpecialEnum.S => "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null", + SpecialEnum.T => "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007", + SpecialEnum.U => "Hello 世界", + SpecialEnum.V => "café", + SpecialEnum.W => "🚀", + SpecialEnum.X => "\\\\n", + SpecialEnum.Y => "\\\\", + SpecialEnum.Z => "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}", + SpecialEnum.Aa => "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'", + SpecialEnum.Bb => "C:\\\\Users\\\\John\\\\Documents\\\\file.txt", + SpecialEnum.Cc => "/usr/local/bin/app", + SpecialEnum.Dd => "\\\\d{3}-\\\\d{2}-\\\\d{4}", + SpecialEnum.Ee => "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}", + SpecialEnum.Ff => "transcript[transcriptType=\"final\"]", + SpecialEnum.Gg => "transcript[transcriptType='final']", + _ => throw new global::System.ArgumentOutOfRangeException( + nameof(value), + value, + null + ), + } + ); + } +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs index a02c6320f5a8..d30564718676 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs @@ -1,10 +1,9 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -using SeedEnum.Core; namespace SeedEnum; -[JsonConverter(typeof(EnumSerializer))] +[JsonConverter(typeof(StatusSerializer))] public enum Status { [EnumMember(Value = "Known")] @@ -13,3 +12,43 @@ public enum Status [EnumMember(Value = "Unknown")] Unknown, } + +internal class StatusSerializer : global::System.Text.Json.Serialization.JsonConverter +{ + public override Status Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return stringValue switch + { + "Known" => Status.Known, + "Unknown" => Status.Unknown, + _ => default, + }; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + Status value, + global::System.Text.Json.JsonSerializerOptions options + ) + { + writer.WriteStringValue( + value switch + { + Status.Known => "Known", + Status.Unknown => "Unknown", + _ => throw new global::System.ArgumentOutOfRangeException( + nameof(value), + value, + null + ), + } + ); + } +} diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dfe652126532..000000000000 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExhaustive.Core; - -namespace SeedExhaustive.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/StringEnumSerializer.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/StringEnumSerializer.cs deleted file mode 100644 index a6c0d9acc65b..000000000000 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExhaustive.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs index 85f28a20b801..1c0b60708427 100644 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints.Put; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCategory.ErrorCategorySerializer))] [Serializable] public readonly record struct ErrorCategory : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ErrorCategory(string value) => new(value); + internal class ErrorCategorySerializer : JsonConverter + { + public override ErrorCategory Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCategory(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCategory value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs index 3324c07f4aa2..0dee283853ff 100644 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints.Put; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCode.ErrorCodeSerializer))] [Serializable] public readonly record struct ErrorCode : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator ErrorCode(string value) => new(value); + internal class ErrorCodeSerializer : JsonConverter + { + public override ErrorCode Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCode(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCode value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs index 1d909ce5b77c..d4ecc0eb51bc 100644 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Types.Enum; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WeatherReport.WeatherReportSerializer))] [Serializable] public readonly record struct WeatherReport : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator WeatherReport(string value) => new(value); + internal class WeatherReportSerializer : JsonConverter + { + public override WeatherReport Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WeatherReport(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WeatherReport value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dfe652126532..000000000000 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExhaustive.Core; - -namespace SeedExhaustive.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/StringEnumSerializer.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/StringEnumSerializer.cs deleted file mode 100644 index a6c0d9acc65b..000000000000 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExhaustive.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs index f2345ce44866..c9c55248949b 100644 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCategory.ErrorCategorySerializer))] [Serializable] public readonly record struct ErrorCategory : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ErrorCategory(string value) => new(value); + internal class ErrorCategorySerializer : JsonConverter + { + public override ErrorCategory Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCategory(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCategory value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs index 4daf8d0a2a36..66403c0f64de 100644 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCode.ErrorCodeSerializer))] [Serializable] public readonly record struct ErrorCode : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator ErrorCode(string value) => new(value); + internal class ErrorCodeSerializer : JsonConverter + { + public override ErrorCode Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCode(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCode value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs index a9a3f5183f39..bf9ef27df567 100644 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Types; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WeatherReport.WeatherReportSerializer))] [Serializable] public readonly record struct WeatherReport : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator WeatherReport(string value) => new(value); + internal class WeatherReportSerializer : JsonConverter + { + public override WeatherReport Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WeatherReport(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WeatherReport value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dfe652126532..000000000000 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExhaustive.Core; - -namespace SeedExhaustive.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/StringEnumSerializer.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/StringEnumSerializer.cs deleted file mode 100644 index a6c0d9acc65b..000000000000 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExhaustive.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs index f2345ce44866..c9c55248949b 100644 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCategory.ErrorCategorySerializer))] [Serializable] public readonly record struct ErrorCategory : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ErrorCategory(string value) => new(value); + internal class ErrorCategorySerializer : JsonConverter + { + public override ErrorCategory Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCategory(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCategory value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs index 4daf8d0a2a36..66403c0f64de 100644 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCode.ErrorCodeSerializer))] [Serializable] public readonly record struct ErrorCode : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator ErrorCode(string value) => new(value); + internal class ErrorCodeSerializer : JsonConverter + { + public override ErrorCode Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCode(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCode value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs index a9a3f5183f39..bf9ef27df567 100644 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Types; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WeatherReport.WeatherReportSerializer))] [Serializable] public readonly record struct WeatherReport : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator WeatherReport(string value) => new(value); + internal class WeatherReportSerializer : JsonConverter + { + public override WeatherReport Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WeatherReport(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WeatherReport value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dfe652126532..000000000000 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExhaustive.Core; - -namespace SeedExhaustive.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/StringEnumSerializer.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/StringEnumSerializer.cs deleted file mode 100644 index a6c0d9acc65b..000000000000 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExhaustive.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs index f2345ce44866..c9c55248949b 100644 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCategory.ErrorCategorySerializer))] [Serializable] public readonly record struct ErrorCategory : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ErrorCategory(string value) => new(value); + internal class ErrorCategorySerializer : JsonConverter + { + public override ErrorCategory Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCategory(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCategory value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs index 4daf8d0a2a36..66403c0f64de 100644 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCode.ErrorCodeSerializer))] [Serializable] public readonly record struct ErrorCode : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator ErrorCode(string value) => new(value); + internal class ErrorCodeSerializer : JsonConverter + { + public override ErrorCode Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCode(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCode value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs index a9a3f5183f39..bf9ef27df567 100644 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Types; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WeatherReport.WeatherReportSerializer))] [Serializable] public readonly record struct WeatherReport : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator WeatherReport(string value) => new(value); + internal class WeatherReportSerializer : JsonConverter + { + public override WeatherReport Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WeatherReport(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WeatherReport value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dfe652126532..000000000000 --- a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExhaustive.Core; - -namespace SeedExhaustive.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Core/StringEnumSerializer.cs b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Core/StringEnumSerializer.cs deleted file mode 100644 index a6c0d9acc65b..000000000000 --- a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExhaustive.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs index f2345ce44866..c9c55248949b 100644 --- a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs +++ b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCategory.ErrorCategorySerializer))] [Serializable] public readonly record struct ErrorCategory : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ErrorCategory(string value) => new(value); + internal class ErrorCategorySerializer : JsonConverter + { + public override ErrorCategory Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCategory(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCategory value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs index 4daf8d0a2a36..66403c0f64de 100644 --- a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs +++ b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCode.ErrorCodeSerializer))] [Serializable] public readonly record struct ErrorCode : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator ErrorCode(string value) => new(value); + internal class ErrorCodeSerializer : JsonConverter + { + public override ErrorCode Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCode(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCode value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs index a9a3f5183f39..bf9ef27df567 100644 --- a/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs +++ b/seed/csharp-sdk/exhaustive/redact-response-body-on-error/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Types; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WeatherReport.WeatherReportSerializer))] [Serializable] public readonly record struct WeatherReport : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator WeatherReport(string value) => new(value); + internal class WeatherReportSerializer : JsonConverter + { + public override WeatherReport Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WeatherReport(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WeatherReport value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dfe652126532..000000000000 --- a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExhaustive.Core; - -namespace SeedExhaustive.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Core/StringEnumSerializer.cs b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Core/StringEnumSerializer.cs deleted file mode 100644 index a6c0d9acc65b..000000000000 --- a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExhaustive.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs index f2345ce44866..c9c55248949b 100644 --- a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs +++ b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCategory.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCategory.ErrorCategorySerializer))] [Serializable] public readonly record struct ErrorCategory : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ErrorCategory(string value) => new(value); + internal class ErrorCategorySerializer : JsonConverter + { + public override ErrorCategory Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCategory(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCategory value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs index 4daf8d0a2a36..66403c0f64de 100644 --- a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs +++ b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Endpoints/Put/Types/ErrorCode.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCode.ErrorCodeSerializer))] [Serializable] public readonly record struct ErrorCode : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator ErrorCode(string value) => new(value); + internal class ErrorCodeSerializer : JsonConverter + { + public override ErrorCode Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCode(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCode value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs index a9a3f5183f39..bf9ef27df567 100644 --- a/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs +++ b/seed/csharp-sdk/exhaustive/use-undiscriminated-unions/src/SeedExhaustive/Types/Enum/Types/WeatherReport.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Types; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WeatherReport.WeatherReportSerializer))] [Serializable] public readonly record struct WeatherReport : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator WeatherReport(string value) => new(value); + internal class WeatherReportSerializer : JsonConverter + { + public override WeatherReport Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WeatherReport(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WeatherReport value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// From de4ed4ee1d497715dff80b95c19af2fd1a6373ff Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:21:16 -0400 Subject: [PATCH 07/29] chore(csharp): update csharp-sdk seed (#13523) Co-authored-by: fern-support --- .../src/SeedExamples.Test/Unit/Serialization/ActressTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/DataTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/EntityTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs | 1 + .../SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs | 1 + .../SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/FileTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MomentTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MovieTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/NodeTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/RequestTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/TestTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/TreeTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/ActressTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/DataTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/EntityTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs | 1 + .../SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs | 1 + .../SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/FileTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MomentTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/MovieTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/NodeTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/RequestTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/TestTest.cs | 1 + .../src/SeedExamples.Test/Unit/Serialization/TreeTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs | 1 + .../src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs | 1 + .../Unit/Serialization/DirectoryTest.cs | 1 + .../SeedObjectsWithImports.Test/Unit/Serialization/FileTest.cs | 1 + .../Unit/Serialization/MetadataTest.cs | 1 + .../SeedObjectsWithImports.Test/Unit/Serialization/NodeTest.cs | 1 + .../SeedObjectsWithImports.Test/Unit/Serialization/TreeTest.cs | 1 + 45 files changed, 45 insertions(+) diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs index 854f82c0c11d..ab28c4d8aa95 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ActressTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs index 837531c2b4ba..b1f42a9eb3ab 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CronJobTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs index 7f6d56f88c57..4094a5e78f3e 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DataTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs index 41ce999a043d..8bd410b276f8 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DirectoryTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs index 1b98b2b5ffeb..9a9f53a1b4b0 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class EntityTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs index a65af102098a..19a8b1558a20 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class EventInfoTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs index 9f9ff5d075a4..5b2eec9e2dd0 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ExceptionInfoTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs index b60f57fd5b14..aab502efbf98 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ExceptionTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs index 1f2e08f15e96..967adfe82875 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ExtendedMovieTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs index e408496013fd..d08b93541713 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class FileTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs index a2735be534ee..30568e6e4bb7 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MetadataTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs index c28bd4282f7e..8933c574d853 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MigrationTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs index d3a2f8eae70c..908d490bf3dc 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs @@ -7,6 +7,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MomentTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs index d7fe712b6879..d50f75203e62 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MovieTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs index 3f51fba7d849..67ab75d87dff 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class NodeTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs index 4828308f6419..56455bd7d334 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class RequestTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs index 1edd77b8953b..a7ac3a45e136 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ResponseTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs index a06a56c44848..211dde344d44 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class TestTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs index 7b216b48102b..f56ac74bfffd 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class TreeTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs index 854f82c0c11d..ab28c4d8aa95 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ActressTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ActressTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs index 837531c2b4ba..b1f42a9eb3ab 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/CronJobTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CronJobTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs index 7f6d56f88c57..4094a5e78f3e 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DataTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DataTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs index 41ce999a043d..8bd410b276f8 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/DirectoryTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DirectoryTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs index 1b98b2b5ffeb..9a9f53a1b4b0 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EntityTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class EntityTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs index a65af102098a..19a8b1558a20 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/EventInfoTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class EventInfoTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs index 9f9ff5d075a4..5b2eec9e2dd0 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionInfoTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ExceptionInfoTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs index b60f57fd5b14..aab502efbf98 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExceptionTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ExceptionTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs index 1f2e08f15e96..967adfe82875 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ExtendedMovieTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ExtendedMovieTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs index e408496013fd..d08b93541713 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/FileTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class FileTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs index a2735be534ee..30568e6e4bb7 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MetadataTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MetadataTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs index c28bd4282f7e..8933c574d853 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MigrationTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MigrationTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs index d3a2f8eae70c..908d490bf3dc 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MomentTest.cs @@ -7,6 +7,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MomentTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs index d7fe712b6879..d50f75203e62 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/MovieTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MovieTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs index 3f51fba7d849..67ab75d87dff 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/NodeTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class NodeTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs index 4828308f6419..56455bd7d334 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/RequestTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class RequestTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs index 1edd77b8953b..a7ac3a45e136 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/ResponseTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ResponseTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs index a06a56c44848..211dde344d44 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TestTest.cs @@ -5,6 +5,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class TestTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs index 7b216b48102b..f56ac74bfffd 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Unit/Serialization/TreeTest.cs @@ -6,6 +6,7 @@ namespace SeedExamples.Test_; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class TreeTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs index fc9a5d7628d6..50e1734c57c3 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/CreateMovieTest.cs @@ -6,6 +6,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreateMovieTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs index bf7defd3127a..6bbf5029779f 100644 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs +++ b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Unit/MockServer/Imdb/GetMovieTest.cs @@ -5,6 +5,7 @@ namespace SeedApi.Test.Unit.MockServer.Imdb; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetMovieTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/DirectoryTest.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/DirectoryTest.cs index d6d5afddfc3c..329ce852f478 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/DirectoryTest.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/DirectoryTest.cs @@ -5,6 +5,7 @@ namespace SeedObjectsWithImports.Test; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DirectoryTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/FileTest.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/FileTest.cs index 6363a696a3e4..23eb9b4e76f6 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/FileTest.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/FileTest.cs @@ -5,6 +5,7 @@ namespace SeedObjectsWithImports.Test; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class FileTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/MetadataTest.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/MetadataTest.cs index c32a70636e3d..ab54a8eb70c6 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/MetadataTest.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/MetadataTest.cs @@ -6,6 +6,7 @@ namespace SeedObjectsWithImports.Test; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class MetadataTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/NodeTest.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/NodeTest.cs index 020cb03794ee..95a79e038fa7 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/NodeTest.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/NodeTest.cs @@ -7,6 +7,7 @@ namespace SeedObjectsWithImports.Test; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class NodeTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/TreeTest.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/TreeTest.cs index 159de2801958..6d6b1a88ad45 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/TreeTest.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Unit/Serialization/TreeTest.cs @@ -7,6 +7,7 @@ namespace SeedObjectsWithImports.Test; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class TreeTest { [NUnit.Framework.Test] From 2efb1deafc4dd37d995126c49390f43d6f10dc47 Mon Sep 17 00:00:00 2001 From: patrick thornton <70873350+patrickthornton@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:29:04 -0400 Subject: [PATCH 08/29] feat(csharp): add `sln-format` config option for legacy .sln solution files (#13460) * feat(csharp): add use-sln-format config option for legacy .sln solution files Co-Authored-By: patrick * fix(csharp): update seed build scripts to find both .slnx and .sln files Co-Authored-By: patrick * chore(csharp): fix biome formatting for join() calls Co-Authored-By: patrick * fix(csharp): remove .sln from .gitignore so seed fixture includes the generated file Co-Authored-By: patrick * feat(csharp): rename config to sln-format enum, generate both .sln and .slnx when sln Co-Authored-By: patrick --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../csharp/base/src/project/CsharpProject.ts | 67 +- .../codegen/src/context/generation-info.ts | 2 + .../src/custom-config/CsharpConfigSchema.ts | 8 +- generators/csharp/sdk/versions.yml | 20 +- seed/csharp-sdk/.gitignore | 2 +- seed/csharp-sdk/seed.yml | 23 +- .../simple-api/use-sln-format/.editorconfig | 35 + .../use-sln-format/.fern/metadata.json | 10 + .../use-sln-format/.github/workflows/ci.yml | 52 + .../simple-api/use-sln-format/.gitignore | 484 +++++++ .../simple-api/use-sln-format/README.md | 170 +++ .../use-sln-format/SeedSimpleApi.sln | 24 + .../use-sln-format/SeedSimpleApi.slnx | 4 + .../simple-api/use-sln-format/reference.md | 42 + .../simple-api/use-sln-format/snippet.json | 17 + .../src/SeedApi.DynamicSnippets/Example0.cs | 20 + .../SeedApi.DynamicSnippets.csproj | 13 + .../Core/HeadersBuilderTests.cs | 326 +++++ .../Core/Json/AdditionalPropertiesTests.cs | 365 ++++++ .../Core/Json/DateOnlyJsonTests.cs | 76 ++ .../Core/Json/DateTimeJsonTests.cs | 110 ++ .../Core/Json/JsonAccessAttributeTests.cs | 160 +++ .../Core/Json/StringEnumSerializerTests.cs | 138 ++ .../Core/QueryStringBuilderTests.cs | 560 ++++++++ .../Core/QueryStringConverterTests.cs | 158 +++ .../Core/RawClientTests/MultipartFormTests.cs | 1121 +++++++++++++++++ .../RawClientTests/QueryParameterTests.cs | 108 ++ .../Core/RawClientTests/RetriesTests.cs | 405 ++++++ .../Core/WithRawResponseTests.cs | 269 ++++ .../SeedSimpleApi.Test.Custom.props | 6 + .../SeedSimpleApi.Test.csproj | 39 + .../src/SeedSimpleApi.Test/TestClient.cs | 6 + .../Unit/MockServer/BaseMockServerTest.cs | 39 + .../Unit/MockServer/User/GetTest.cs | 33 + .../Utils/AdditionalPropertiesComparer.cs | 126 ++ .../SeedSimpleApi.Test/Utils/JsonAssert.cs | 19 + .../Utils/JsonElementComparer.cs | 236 ++++ .../Utils/NUnitExtensions.cs | 30 + .../SeedSimpleApi.Test/Utils/OneOfComparer.cs | 86 ++ .../Utils/OptionalComparer.cs | 104 ++ .../Utils/ReadOnlyMemoryComparer.cs | 87 ++ .../src/SeedSimpleApi/Core/ApiResponse.cs | 13 + .../src/SeedSimpleApi/Core/BaseRequest.cs | 67 + .../Core/CollectionItemSerializer.cs | 89 ++ .../src/SeedSimpleApi/Core/Constants.cs | 7 + .../SeedSimpleApi/Core/DateOnlyConverter.cs | 747 +++++++++++ .../SeedSimpleApi/Core/DateTimeSerializer.cs | 22 + .../src/SeedSimpleApi/Core/EmptyRequest.cs | 11 + .../src/SeedSimpleApi/Core/EncodingCache.cs | 11 + .../src/SeedSimpleApi/Core/Extensions.cs | 55 + .../src/SeedSimpleApi/Core/FormUrlEncoder.cs | 33 + .../src/SeedSimpleApi/Core/HeaderValue.cs | 52 + .../src/SeedSimpleApi/Core/Headers.cs | 28 + .../src/SeedSimpleApi/Core/HeadersBuilder.cs | 197 +++ .../Core/HttpContentExtensions.cs | 20 + .../Core/HttpMethodExtensions.cs | 8 + .../SeedSimpleApi/Core/IIsRetryableContent.cs | 6 + .../src/SeedSimpleApi/Core/IRequestOptions.cs | 83 ++ .../SeedSimpleApi/Core/JsonAccessAttribute.cs | 15 + .../SeedSimpleApi/Core/JsonConfiguration.cs | 275 ++++ .../src/SeedSimpleApi/Core/JsonRequest.cs | 36 + .../Core/MultipartFormRequest.cs | 294 +++++ .../SeedSimpleApi/Core/NullableAttribute.cs | 18 + .../src/SeedSimpleApi/Core/OneOfSerializer.cs | 145 +++ .../src/SeedSimpleApi/Core/Optional.cs | 474 +++++++ .../SeedSimpleApi/Core/OptionalAttribute.cs | 17 + .../Core/Public/AdditionalProperties.cs | 353 ++++++ .../Core/Public/ClientOptions.cs | 84 ++ .../Core/Public/FileParameter.cs | 63 + .../SeedSimpleApi/Core/Public/RawResponse.cs | 24 + .../Core/Public/RequestOptions.cs | 86 ++ .../Core/Public/SeedSimpleApiApiException.cs | 22 + .../Core/Public/SeedSimpleApiEnvironment.cs | 9 + .../Core/Public/SeedSimpleApiException.cs | 7 + .../src/SeedSimpleApi/Core/Public/Version.cs | 7 + .../Core/Public/WithRawResponse.cs | 18 + .../Core/Public/WithRawResponseTask.cs | 144 +++ .../SeedSimpleApi/Core/QueryStringBuilder.cs | 484 +++++++ .../Core/QueryStringConverter.cs | 259 ++++ .../src/SeedSimpleApi/Core/RawClient.cs | 344 +++++ .../src/SeedSimpleApi/Core/RawResponse.cs | 24 + .../src/SeedSimpleApi/Core/ResponseHeaders.cs | 108 ++ .../src/SeedSimpleApi/Core/StreamRequest.cs | 29 + .../src/SeedSimpleApi/Core/StringEnum.cs | 6 + .../Core/StringEnumExtensions.cs | 6 + .../Core/StringEnumSerializer.cs | 25 + .../src/SeedSimpleApi/Core/ValueConvert.cs | 114 ++ .../src/SeedSimpleApi/ISeedSimpleApiClient.cs | 6 + .../SeedSimpleApi/SeedSimpleApi.Custom.props | 20 + .../src/SeedSimpleApi/SeedSimpleApi.csproj | 58 + .../src/SeedSimpleApi/SeedSimpleApiClient.cs | 41 + .../src/SeedSimpleApi/User/IUserClient.cs | 10 + .../src/SeedSimpleApi/User/Types/User.cs | 34 + .../src/SeedSimpleApi/User/UserClient.cs | 91 ++ 94 files changed, 10781 insertions(+), 18 deletions(-) create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/.editorconfig create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/.fern/metadata.json create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/.github/workflows/ci.yml create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/.gitignore create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/README.md create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.sln create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.slnx create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/reference.md create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/snippet.json create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/Example0.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/HeadersBuilderTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/AdditionalPropertiesTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateOnlyJsonTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateTimeJsonTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/JsonAccessAttributeTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringBuilderTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringConverterTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/MultipartFormTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/QueryParameterTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/WithRawResponseTests.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.Custom.props create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.csproj create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/TestClient.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/AdditionalPropertiesComparer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonAssert.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonElementComparer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/NUnitExtensions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OneOfComparer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OptionalComparer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/ReadOnlyMemoryComparer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ApiResponse.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/BaseRequest.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/CollectionItemSerializer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Constants.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateOnlyConverter.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateTimeSerializer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EmptyRequest.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EncodingCache.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Extensions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/FormUrlEncoder.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeaderValue.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Headers.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeadersBuilder.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpContentExtensions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpMethodExtensions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IIsRetryableContent.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IRequestOptions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonAccessAttribute.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonConfiguration.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonRequest.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/MultipartFormRequest.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/NullableAttribute.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OneOfSerializer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Optional.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OptionalAttribute.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/AdditionalProperties.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/ClientOptions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/FileParameter.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RawResponse.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RequestOptions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiApiException.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiEnvironment.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiException.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/Version.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponse.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponseTask.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringBuilder.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringConverter.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawClient.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawResponse.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ResponseHeaders.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StreamRequest.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnum.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumExtensions.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ValueConvert.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/ISeedSimpleApiClient.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.Custom.props create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.csproj create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/IUserClient.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/Types/User.cs create mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/UserClient.cs diff --git a/generators/csharp/base/src/project/CsharpProject.ts b/generators/csharp/base/src/project/CsharpProject.ts index f42ed80d588c..9c5fad2b3830 100644 --- a/generators/csharp/base/src/project/CsharpProject.ts +++ b/generators/csharp/base/src/project/CsharpProject.ts @@ -2,6 +2,7 @@ import { AbstractProject, FernGeneratorExec, File, SourceFetcher } from "@fern-a import { Generation, WithGeneration } from "@fern-api/csharp-codegen"; import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils"; import { loggingExeca } from "@fern-api/logging-execa"; +import { createHash } from "crypto"; import { Eta } from "eta"; import { mkdir, readFile, unlink, writeFile } from "fs/promises"; import path from "path"; @@ -153,7 +154,7 @@ export class CsharpProject extends AbstractProject { .replace(/<\/Project>/, ``) ); - // call dotnet format on the solution file using absolute path + // call dotnet format on the solution file using absolute path (always use .slnx for dotnet format) const solutionFile = join(absolutePathToSolutionDirectory, RelativeFilePath.of(`${this.name}.slnx`)); await loggingExeca(this.context.logger, "dotnet", ["format", solutionFile, "--severity", "error"], { doNotPipeOutput: false @@ -435,8 +436,10 @@ dotnet_diagnostic.IDE0005.severity = error } /** - * Generates the .slnx solution file directly as XML, avoiding dotnet CLI overhead. + * Generates the solution file directly as a template, avoiding dotnet CLI overhead. * Computes relative paths from the solution directory to both project .csproj files. + * When `sln-format` is "sln", generates both .sln and .slnx files. + * When `sln-format` is "slnx" (default), generates only .slnx. */ private async createSolutionFile({ absolutePathToSolutionDirectory, @@ -456,6 +459,7 @@ dotnet_diagnostic.IDE0005.severity = error RelativeFilePath.of(`${testProjectName}.csproj`) ); + // Always generate .slnx format const libraryCsprojRelative = path .relative(absolutePathToSolutionDirectory, libraryCsprojAbsolute) .replace(/\\/g, "/"); @@ -469,8 +473,53 @@ dotnet_diagnostic.IDE0005.severity = error `; - const solutionFilePath = join(absolutePathToSolutionDirectory, RelativeFilePath.of(`${this.name}.slnx`)); - await writeFile(solutionFilePath, slnxContents); + const slnxFilePath = join(absolutePathToSolutionDirectory, RelativeFilePath.of(`${this.name}.slnx`)); + await writeFile(slnxFilePath, slnxContents); + + // When sln-format is "sln", also generate the legacy .sln file + if (this.settings.slnFormat === "sln") { + const libraryCsprojRelativeBackslash = path + .relative(absolutePathToSolutionDirectory, libraryCsprojAbsolute) + .replace(/\//g, "\\"); + const testCsprojRelativeBackslash = path + .relative(absolutePathToSolutionDirectory, testCsprojAbsolute) + .replace(/\//g, "\\"); + + const projectTypeGuid = "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"; + const libraryProjectGuid = generateDeterministicGuid(this.name); + const testProjectGuid = generateDeterministicGuid(testProjectName); + + const slnContents = [ + "Microsoft Visual Studio Solution File, Format Version 12.00", + "# Visual Studio Version 17", + "VisualStudioVersion = 17.0.31903.59", + "MinimumVisualStudioVersion = 10.0.40219.1", + `Project("{${projectTypeGuid}}") = "${this.name}", "${libraryCsprojRelativeBackslash}", "{${libraryProjectGuid}}"`, + "EndProject", + `Project("{${projectTypeGuid}}") = "${testProjectName}", "${testCsprojRelativeBackslash}", "{${testProjectGuid}}"`, + "EndProject", + "Global", + "\tGlobalSection(SolutionConfigurationPlatforms) = preSolution", + "\t\tDebug|Any CPU = Debug|Any CPU", + "\t\tRelease|Any CPU = Release|Any CPU", + "\tEndGlobalSection", + "\tGlobalSection(ProjectConfigurationPlatforms) = postSolution", + `\t\t{${libraryProjectGuid}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU`, + `\t\t{${libraryProjectGuid}}.Debug|Any CPU.Build.0 = Debug|Any CPU`, + `\t\t{${libraryProjectGuid}}.Release|Any CPU.ActiveCfg = Release|Any CPU`, + `\t\t{${libraryProjectGuid}}.Release|Any CPU.Build.0 = Release|Any CPU`, + `\t\t{${testProjectGuid}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU`, + `\t\t{${testProjectGuid}}.Debug|Any CPU.Build.0 = Debug|Any CPU`, + `\t\t{${testProjectGuid}}.Release|Any CPU.ActiveCfg = Release|Any CPU`, + `\t\t{${testProjectGuid}}.Release|Any CPU.Build.0 = Release|Any CPU`, + "\tEndGlobalSection", + "EndGlobal", + "" + ].join("\n"); + + const slnFilePath = join(absolutePathToSolutionDirectory, RelativeFilePath.of(`${this.name}.sln`)); + await writeFile(slnFilePath, slnContents); + } } private async createCoreDirectory({ @@ -706,6 +755,16 @@ function getAsIsFilepath(filename: string): string { return AbsoluteFilePath.of(path.join(__dirname, "asIs", filename)); } +/** + * Generates a deterministic GUID from a project name using MD5 hashing. + * This ensures the same project name always produces the same GUID, + * making .sln files reproducible across generation runs. + */ +function generateDeterministicGuid(name: string): string { + const hash = createHash("md5").update(name).digest("hex"); + return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`.toUpperCase(); +} + declare namespace CsProj { interface Args { name: string; diff --git a/generators/csharp/codegen/src/context/generation-info.ts b/generators/csharp/codegen/src/context/generation-info.ts index 5d4a648dc31a..68f7c367a2e1 100644 --- a/generators/csharp/codegen/src/context/generation-info.ts +++ b/generators/csharp/codegen/src/context/generation-info.ts @@ -216,6 +216,8 @@ export class Generation { omitFernHeaders: () => this.customConfig["omit-fern-headers"] ?? false, /** When true, uses PascalCase for environment names (e.g., "Production" instead of "production"). Default: true. */ pascalCaseEnvironments: () => this.customConfig["pascal-case-environments"] ?? true, + /** Solution file format: "sln" generates both .sln and .slnx, "slnx" (default) generates only .slnx. */ + slnFormat: () => this.customConfig["sln-format"] ?? "slnx", /** When true, requires explicit namespace declarations instead of using file-scoped namespaces. Default: false. */ explicitNamespaces: () => this.customConfig["explicit-namespaces"] === true, /** diff --git a/generators/csharp/codegen/src/custom-config/CsharpConfigSchema.ts b/generators/csharp/codegen/src/custom-config/CsharpConfigSchema.ts index 280c68cfd474..97f6d825ab99 100644 --- a/generators/csharp/codegen/src/custom-config/CsharpConfigSchema.ts +++ b/generators/csharp/codegen/src/custom-config/CsharpConfigSchema.ts @@ -106,7 +106,13 @@ export const CsharpConfigSchema = z.object({ ), "pascal-case-environments": z.boolean().optional(), - "experimental-enable-forward-compatible-enums": z.boolean().optional() + "experimental-enable-forward-compatible-enums": z.boolean().optional(), + + // Solution file format option. + // "sln" generates both .sln and .slnx files for compatibility with older + // .NET tooling or CI systems that do not yet support .slnx. + // "slnx" (default) generates only the modern .slnx format. + "sln-format": z.enum(["sln", "slnx"]).optional() }); export type CsharpConfigSchema = z.infer; diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index c142b3fa1ba4..1c1661ef5efa 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,22 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.30.0 + changelogEntry: + - summary: | + Add `sln-format` configuration option. Set to `"sln"` to generate both a legacy + `.sln` solution file and the modern `.slnx` file. The default value `"slnx"` generates + only the `.slnx` file. This is useful for teams that need compatibility with older + .NET tooling or CI systems that do not yet support the `.slnx` format. + + ```yaml + generators: + - name: fernapi/fern-csharp-sdk + config: + sln-format: sln + ``` + type: feat + createdAt: "2026-03-13" + irVersion: 65 + - version: 2.29.0 changelogEntry: - summary: | @@ -24,7 +42,6 @@ type: feat createdAt: "2026-03-13" irVersion: 65 - - version: 2.27.1 changelogEntry: - summary: | @@ -43,7 +60,6 @@ type: feat createdAt: "2026-03-13" irVersion: 65 - - version: 2.26.0 changelogEntry: - summary: | diff --git a/seed/csharp-sdk/.gitignore b/seed/csharp-sdk/.gitignore index afef80f0b0af..8b137891791f 100644 --- a/seed/csharp-sdk/.gitignore +++ b/seed/csharp-sdk/.gitignore @@ -1 +1 @@ -**/*.sln + diff --git a/seed/csharp-sdk/seed.yml b/seed/csharp-sdk/seed.yml index aebd33da1be9..fc2588283b30 100644 --- a/seed/csharp-sdk/seed.yml +++ b/seed/csharp-sdk/seed.yml @@ -10,18 +10,18 @@ buildScripts: compileScript: commands: - | - solution_file="$(find . -maxdepth 4 -name '*.slnx' | head -n 1)" + solution_file="$(find . -maxdepth 4 \( -name '*.slnx' -o -name '*.sln' \) | head -n 1)" if [ -z "$solution_file" ]; then - echo "ERROR: No .slnx file found" + echo "ERROR: No .slnx or .sln file found" exit 1 fi dotnet build "$solution_file" -c Release testScript: commands: - | - solution_file="$(find . -maxdepth 4 -name '*.slnx' | head -n 1)" + solution_file="$(find . -maxdepth 4 \( -name '*.slnx' -o -name '*.sln' \) | head -n 1)" if [ -z "$solution_file" ]; then - echo "ERROR: No .slnx file found" + echo "ERROR: No .slnx or .sln file found" exit 1 fi dotnet test "$solution_file" @@ -60,10 +60,10 @@ scripts: # otherwise install .NET 10 command -v dotnet >/dev/null 2>&1 && ver=$(dotnet --version) && major=$(echo "$ver" | cut -d. -f1) && [ "$major" -ge 10 ] || { echo "ERROR: .NET SDK 10+ required (found ${ver:-not found})"; false; } - # Find the solution file - solution_file="$(find . -maxdepth 4 -name '*.slnx' | head -n 1)" + # Find the solution file (.slnx or .sln) + solution_file="$(find . -maxdepth 4 \( -name '*.slnx' -o -name '*.sln' \) | head -n 1)" if [ -z "$solution_file" ]; then - echo "ERROR: No .slnx file found" + echo "ERROR: No .slnx or .sln file found" exit 1 fi @@ -74,10 +74,10 @@ scripts: fi test: - | - # Find the solution file - solution_file="$(find . -maxdepth 4 -name '*.slnx' | head -n 1)" + # Find the solution file (.slnx or .sln) + solution_file="$(find . -maxdepth 4 \( -name '*.slnx' -o -name '*.sln' \) | head -n 1)" if [ -z "$solution_file" ]; then - echo "ERROR: No .slnx file found" + echo "ERROR: No .slnx or .sln file found" exit 1 fi @@ -97,6 +97,9 @@ fixtures: test: test/SeedApi.Test solution: . other: lib/SeedApi + - outputFolder: use-sln-format + customConfig: + sln-format: sln literal: - customConfig: null outputFolder: no-custom-config diff --git a/seed/csharp-sdk/simple-api/use-sln-format/.editorconfig b/seed/csharp-sdk/simple-api/use-sln-format/.editorconfig new file mode 100644 index 000000000000..1e7a0adbac80 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/.editorconfig @@ -0,0 +1,35 @@ +root = true + +[*.cs] +resharper_arrange_object_creation_when_type_evident_highlighting = hint +resharper_auto_property_can_be_made_get_only_global_highlighting = hint +resharper_check_namespace_highlighting = hint +resharper_class_never_instantiated_global_highlighting = hint +resharper_class_never_instantiated_local_highlighting = hint +resharper_collection_never_updated_global_highlighting = hint +resharper_convert_type_check_pattern_to_null_check_highlighting = hint +resharper_inconsistent_naming_highlighting = hint +resharper_member_can_be_private_global_highlighting = hint +resharper_member_hides_static_from_outer_class_highlighting = hint +resharper_not_accessed_field_local_highlighting = hint +resharper_nullable_warning_suppression_is_used_highlighting = suggestion +resharper_partial_type_with_single_part_highlighting = hint +resharper_prefer_concrete_value_over_default_highlighting = none +resharper_private_field_can_be_converted_to_local_variable_highlighting = hint +resharper_property_can_be_made_init_only_global_highlighting = hint +resharper_property_can_be_made_init_only_local_highlighting = hint +resharper_redundant_name_qualifier_highlighting = none +resharper_redundant_using_directive_highlighting = hint +resharper_replace_slice_with_range_indexer_highlighting = none +resharper_unused_auto_property_accessor_global_highlighting = hint +resharper_unused_auto_property_accessor_local_highlighting = hint +resharper_unused_member_global_highlighting = hint +resharper_unused_type_global_highlighting = hint +resharper_use_string_interpolation_highlighting = hint +dotnet_diagnostic.CS1591.severity = suggestion + +[src/**/Types/*.cs] +resharper_check_namespace_highlighting = none + +[src/**/Core/Public/*.cs] +resharper_check_namespace_highlighting = none \ No newline at end of file diff --git a/seed/csharp-sdk/simple-api/use-sln-format/.fern/metadata.json b/seed/csharp-sdk/simple-api/use-sln-format/.fern/metadata.json new file mode 100644 index 000000000000..7c9378c6337c --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/.fern/metadata.json @@ -0,0 +1,10 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-csharp-sdk", + "generatorVersion": "latest", + "generatorConfig": { + "sln-format": "sln" + }, + "originGitCommit": "DUMMY", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/csharp-sdk/simple-api/use-sln-format/.github/workflows/ci.yml b/seed/csharp-sdk/simple-api/use-sln-format/.github/workflows/ci.yml new file mode 100644 index 000000000000..e3066d1f512f --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + DOTNET_NOLOGO: true + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.x + + - name: Install tools + run: dotnet tool restore + + - name: Restore dependencies + run: dotnet restore src/SeedSimpleApi/SeedSimpleApi.csproj + + - name: Build + run: dotnet build src/SeedSimpleApi/SeedSimpleApi.csproj --no-restore -c Release + + - name: Restore test dependencies + run: dotnet restore src/SeedSimpleApi.Test/SeedSimpleApi.Test.csproj + + - name: Build tests + run: dotnet build src/SeedSimpleApi.Test/SeedSimpleApi.Test.csproj --no-restore -c Release + + - name: Test + run: dotnet test src/SeedSimpleApi.Test/SeedSimpleApi.Test.csproj --no-restore --no-build -c Release + + - name: Pack + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + run: dotnet pack src/SeedSimpleApi/SeedSimpleApi.csproj --no-build --no-restore -c Release + + - name: Publish to NuGet.org + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} + run: dotnet nuget push src/SeedSimpleApi/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" + diff --git a/seed/csharp-sdk/simple-api/use-sln-format/.gitignore b/seed/csharp-sdk/simple-api/use-sln-format/.gitignore new file mode 100644 index 000000000000..11014f2b33d7 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +## This is based on `dotnet new gitignore` and customized by Fern + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +# [Rr]elease/ (Ignored by Fern) +# [Rr]eleases/ (Ignored by Fern) +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +# [Ll]og/ (Ignored by Fern) +# [Ll]ogs/ (Ignored by Fern) + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/seed/csharp-sdk/simple-api/use-sln-format/README.md b/seed/csharp-sdk/simple-api/use-sln-format/README.md new file mode 100644 index 000000000000..1d161be93ec3 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/README.md @@ -0,0 +1,170 @@ +# Seed C# Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FC%23) +[![nuget shield](https://img.shields.io/nuget/v/SeedSimpleApi)](https://nuget.org/packages/SeedSimpleApi) + +The Seed C# library provides convenient access to the Seed APIs from C#. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Raw Response](#raw-response) + - [Additional Headers](#additional-headers) + - [Additional Query Parameters](#additional-query-parameters) +- [Contributing](#contributing) + +## Requirements + +This SDK requires: + +## Installation + +```sh +dotnet add package SeedSimpleApi +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```csharp +using SeedSimpleApi; + +var client = new SeedSimpleApiClient("TOKEN"); +await client.User.GetAsync("id"); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```csharp +using SeedSimpleApi; + +try { + var response = await client.User.GetAsync(...); +} catch (SeedSimpleApiApiException e) { + System.Console.WriteLine(e.Body); + System.Console.WriteLine(e.StatusCode); +} +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `MaxRetries` request option to configure this behavior. + +```csharp +var response = await client.User.GetAsync( + ..., + new RequestOptions { + MaxRetries: 0 // Override MaxRetries at the request level + } +); +``` + +### Timeouts + +The SDK defaults to a 30 second timeout. Use the `Timeout` option to configure this behavior. + +```csharp +var response = await client.User.GetAsync( + ..., + new RequestOptions { + Timeout: TimeSpan.FromSeconds(3) // Override timeout to 3s + } +); +``` + +### Raw Response + +Access raw HTTP response data (status code, headers, URL) alongside parsed response data using the `.WithRawResponse()` method. + +```csharp +using SeedSimpleApi; + +// Access raw response data (status code, headers, etc.) alongside the parsed response +var result = await client.User.GetAsync(...).WithRawResponse(); + +// Access the parsed data +var data = result.Data; + +// Access raw response metadata +var statusCode = result.RawResponse.StatusCode; +var headers = result.RawResponse.Headers; +var url = result.RawResponse.Url; + +// Access specific headers (case-insensitive) +if (headers.TryGetValue("X-Request-Id", out var requestId)) +{ + System.Console.WriteLine($"Request ID: {requestId}"); +} + +// For the default behavior, simply await without .WithRawResponse() +var data = await client.User.GetAsync(...); +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. + +```csharp +var response = await client.User.GetAsync( + ..., + new RequestOptions { + AdditionalHeaders = new Dictionary + { + { "X-Custom-Header", "custom-value" } + } + } +); +``` + +### Additional Query Parameters + +If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. + +```csharp +var response = await client.User.GetAsync( + ..., + new RequestOptions { + AdditionalQueryParameters = new Dictionary + { + { "custom_param", "custom-value" } + } + } +); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.sln b/seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.sln new file mode 100644 index 000000000000..d6a3fff26251 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeedSimpleApi", "src\SeedSimpleApi\SeedSimpleApi.csproj", "{B03A7A2B-3B0F-2ED7-2B2C-5154CF821312}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeedSimpleApi.Test", "src\SeedSimpleApi.Test\SeedSimpleApi.Test.csproj", "{4EDD4DAE-E1F5-2F72-AB2D-D180CF4C7600}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B03A7A2B-3B0F-2ED7-2B2C-5154CF821312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B03A7A2B-3B0F-2ED7-2B2C-5154CF821312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B03A7A2B-3B0F-2ED7-2B2C-5154CF821312}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B03A7A2B-3B0F-2ED7-2B2C-5154CF821312}.Release|Any CPU.Build.0 = Release|Any CPU + {4EDD4DAE-E1F5-2F72-AB2D-D180CF4C7600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EDD4DAE-E1F5-2F72-AB2D-D180CF4C7600}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EDD4DAE-E1F5-2F72-AB2D-D180CF4C7600}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EDD4DAE-E1F5-2F72-AB2D-D180CF4C7600}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.slnx b/seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.slnx new file mode 100644 index 000000000000..838bfb08ae1a --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/SeedSimpleApi.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/seed/csharp-sdk/simple-api/use-sln-format/reference.md b/seed/csharp-sdk/simple-api/use-sln-format/reference.md new file mode 100644 index 000000000000..1aeb52377008 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/reference.md @@ -0,0 +1,42 @@ +# Reference +## User +
client.User.GetAsync(id) -> WithRawResponseTask<User> +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```csharp +await client.User.GetAsync("id"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/csharp-sdk/simple-api/use-sln-format/snippet.json b/seed/csharp-sdk/simple-api/use-sln-format/snippet.json new file mode 100644 index 000000000000..1f6a348600c1 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/snippet.json @@ -0,0 +1,17 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": null, + "id": { + "path": "/users/{id}", + "method": "GET", + "identifier_override": "endpoint_user.get" + }, + "snippet": { + "type": "csharp", + "client": "using SeedSimpleApi;\n\nvar client = new SeedSimpleApiClient(\"TOKEN\");\nawait client.User.GetAsync(\"id\");\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/Example0.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/Example0.cs new file mode 100644 index 000000000000..432b854db2c5 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/Example0.cs @@ -0,0 +1,20 @@ +using SeedSimpleApi; + +namespace Usage; + +public class Example0 +{ + public async Task Do() { + var client = new SeedSimpleApiClient( + token: "", + clientOptions: new ClientOptions { + BaseUrl = "https://api.fern.com" + } + ); + + await client.User.GetAsync( + "id" + ); + } + +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj new file mode 100644 index 000000000000..3417db2e58e2 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedApi.DynamicSnippets/SeedApi.DynamicSnippets.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + 12 + enable + enable + + + + + + \ No newline at end of file diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/HeadersBuilderTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/HeadersBuilderTests.cs new file mode 100644 index 000000000000..cacc2dd56fe7 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/HeadersBuilderTests.cs @@ -0,0 +1,326 @@ +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core; + +[TestFixture] +public class HeadersBuilderTests +{ + [Test] + public async global::System.Threading.Tasks.Task Add_SimpleHeaders() + { + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") + .Add("Authorization", "Bearer token123") + .Add("X-API-Key", "key456") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Content-Type"], Is.EqualTo("application/json")); + Assert.That(headers["Authorization"], Is.EqualTo("Bearer token123")); + Assert.That(headers["X-API-Key"], Is.EqualTo("key456")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_NullValuesIgnored() + { + var headers = await new HeadersBuilder.Builder() + .Add("Header1", "value1") + .Add("Header2", null) + .Add("Header3", "value3") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(2)); + Assert.That(headers.ContainsKey("Header1"), Is.True); + Assert.That(headers.ContainsKey("Header2"), Is.False); + Assert.That(headers.ContainsKey("Header3"), Is.True); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_OverwritesExistingHeader() + { + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") + .Add("Content-Type", "application/xml") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers["Content-Type"], Is.EqualTo("application/xml")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_MergesExistingHeaders() + { + var existingHeaders = new Headers( + new Dictionary { { "Header1", "value1" }, { "Header2", "value2" } } + ); + + var result = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result["Header1"], Is.EqualTo("value1")); + Assert.That(result["Header2"], Is.EqualTo("value2")); + Assert.That(result["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_OverwritesExistingHeaders() + { + var existingHeaders = new Headers( + new Dictionary { { "Header1", "override" } } + ); + + var result = await new HeadersBuilder.Builder() + .Add("Header1", "original") + .Add("Header2", "keep") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result["Header1"], Is.EqualTo("override")); + Assert.That(result["Header2"], Is.EqualTo("keep")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_NullHeadersIgnored() + { + var result = await new HeadersBuilder.Builder() + .Add("Header1", "value1") + .Add((Headers?)null) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result["Header1"], Is.EqualTo("value1")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_KeyValuePairOverload_AddsHeaders() + { + var additionalHeaders = new List> + { + new("Header1", "value1"), + new("Header2", "value2"), + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(additionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + Assert.That(headers["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_KeyValuePairOverload_IgnoresNullValues() + { + var additionalHeaders = new List> + { + new("Header1", "value1"), + new("Header2", null), // Should be ignored + }; + + var headers = await new HeadersBuilder.Builder() + .Add(additionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers.ContainsKey("Header2"), Is.False); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_DictionaryOverload_AddsHeaders() + { + var dict = new Dictionary + { + { "Header1", "value1" }, + { "Header2", "value2" }, + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Header3", "value3") + .Add(dict) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(3)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + Assert.That(headers["Header3"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task EmptyBuilder_ReturnsEmptyHeaders() + { + var headers = await new HeadersBuilder.Builder().BuildAsync().ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task OnlyNullValues_ReturnsEmptyHeaders() + { + var headers = await new HeadersBuilder.Builder() + .Add("Header1", null) + .Add("Header2", null) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task ComplexMergingScenario() + { + // Simulates real SDK usage: endpoint headers + client headers + request options + var clientHeaders = new Headers( + new Dictionary + { + { "X-Client-Version", "1.0.0" }, + { "User-Agent", "MyClient/1.0" }, + } + ); + + var clientAdditionalHeaders = new List> + { + new("X-Custom-Header", "custom-value"), + }; + + var requestOptionsHeaders = new Headers( + new Dictionary + { + { "Authorization", "Bearer user-token" }, + { "User-Agent", "MyClient/2.0" }, // Override + } + ); + + var requestAdditionalHeaders = new List> + { + new("X-Request-ID", "req-123"), + new("X-Custom-Header", "overridden-value"), // Override + }; + + var headers = await new HeadersBuilder.Builder() + .Add("Content-Type", "application/json") // Endpoint header + .Add("X-Endpoint-ID", "endpoint-1") + .Add(clientHeaders) + .Add(clientAdditionalHeaders) + .Add(requestOptionsHeaders) + .Add(requestAdditionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + + // Verify precedence + Assert.That(headers["Content-Type"], Is.EqualTo("application/json")); + Assert.That(headers["X-Endpoint-ID"], Is.EqualTo("endpoint-1")); + Assert.That(headers["X-Client-Version"], Is.EqualTo("1.0.0")); + Assert.That(headers["User-Agent"], Is.EqualTo("MyClient/2.0")); // Overridden + Assert.That(headers["Authorization"], Is.EqualTo("Bearer user-token")); + Assert.That(headers["X-Request-ID"], Is.EqualTo("req-123")); + Assert.That(headers["X-Custom-Header"], Is.EqualTo("overridden-value")); // Overridden + } + + [Test] + public async global::System.Threading.Tasks.Task Builder_WithCapacity() + { + // Test that capacity constructor works without errors + var headers = await new HeadersBuilder.Builder(capacity: 10) + .Add("Header1", "value1") + .Add("Header2", "value2") + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(2)); + Assert.That(headers["Header1"], Is.EqualTo("value1")); + Assert.That(headers["Header2"], Is.EqualTo("value2")); + } + + [Test] + public async global::System.Threading.Tasks.Task Add_HeadersOverload_ResolvesDynamicHeaderValues() + { + // Test that BuildAsync properly resolves HeaderValue instances + var existingHeaders = new Headers(); + existingHeaders["DynamicHeader"] = + (Func>)( + () => global::System.Threading.Tasks.Task.FromResult("dynamic-value") + ); + + var result = await new HeadersBuilder.Builder() + .Add("StaticHeader", "static-value") + .Add(existingHeaders) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result["StaticHeader"], Is.EqualTo("static-value")); + Assert.That(result["DynamicHeader"], Is.EqualTo("dynamic-value")); + } + + [Test] + public async global::System.Threading.Tasks.Task MultipleSyncAdds() + { + var headers1 = new Headers(new Dictionary { { "H1", "v1" } }); + var headers2 = new Headers(new Dictionary { { "H2", "v2" } }); + var headers3 = new Headers(new Dictionary { { "H3", "v3" } }); + + var result = await new HeadersBuilder.Builder() + .Add(headers1) + .Add(headers2) + .Add(headers3) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result["H1"], Is.EqualTo("v1")); + Assert.That(result["H2"], Is.EqualTo("v2")); + Assert.That(result["H3"], Is.EqualTo("v3")); + } + + [Test] + public async global::System.Threading.Tasks.Task PrecedenceOrder_LatestWins() + { + // Test that later operations override earlier ones + var headers1 = new Headers(new Dictionary { { "Key", "value1" } }); + var headers2 = new Headers(new Dictionary { { "Key", "value2" } }); + var additional = new List> { new("Key", "value3") }; + + var result = await new HeadersBuilder.Builder() + .Add("Key", "value0") + .Add(headers1) + .Add(headers2) + .Add(additional) + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(result["Key"], Is.EqualTo("value3")); + } + + [Test] + public async global::System.Threading.Tasks.Task CaseInsensitiveKeys() + { + // Test that header keys are case-insensitive + var headers = await new HeadersBuilder.Builder() + .Add("content-type", "application/json") + .Add("Content-Type", "application/xml") // Should overwrite + .BuildAsync() + .ConfigureAwait(false); + + Assert.That(headers.Count, Is.EqualTo(1)); + Assert.That(headers["content-type"], Is.EqualTo("application/xml")); + Assert.That(headers["Content-Type"], Is.EqualTo("application/xml")); + Assert.That(headers["CONTENT-TYPE"], Is.EqualTo("application/xml")); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/AdditionalPropertiesTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/AdditionalPropertiesTests.cs new file mode 100644 index 000000000000..dea858af1734 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/AdditionalPropertiesTests.cs @@ -0,0 +1,365 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core.Json; + +[TestFixture] +public class AdditionalPropertiesTests +{ + [Test] + public void Record_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "id": "1", + "category": "fiction", + "title": "The Hobbit" + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.Id, Is.EqualTo("1")); + Assert.That(record.AdditionalProperties["category"].GetString(), Is.EqualTo("fiction")); + Assert.That(record.AdditionalProperties["title"].GetString(), Is.EqualTo("The Hobbit")); + }); + } + + [Test] + public void RecordWithWriteableAdditionalProperties_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecord + { + Id = "1", + AdditionalProperties = { ["category"] = "fiction", ["title"] = "The Hobbit" }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.Id, Is.EqualTo("1")); + Assert.That( + deserializedRecord.AdditionalProperties["category"], + Is.InstanceOf() + ); + Assert.That( + ((JsonElement)deserializedRecord.AdditionalProperties["category"]!).GetString(), + Is.EqualTo("fiction") + ); + Assert.That( + deserializedRecord.AdditionalProperties["title"], + Is.InstanceOf() + ); + Assert.That( + ((JsonElement)deserializedRecord.AdditionalProperties["title"]!).GetString(), + Is.EqualTo("The Hobbit") + ); + }); + } + + [Test] + public void ReadOnlyAdditionalProperties_ShouldRetrieveValuesCorrectly() + { + // Arrange + var extensionData = new Dictionary + { + ["key1"] = JsonUtils.SerializeToElement("value1"), + ["key2"] = JsonUtils.SerializeToElement(123), + }; + var readOnlyProps = new ReadOnlyAdditionalProperties(); + readOnlyProps.CopyFromExtensionData(extensionData); + + // Act & Assert + Assert.That(readOnlyProps["key1"].GetString(), Is.EqualTo("value1")); + Assert.That(readOnlyProps["key2"].GetInt32(), Is.EqualTo(123)); + } + + [Test] + public void AdditionalProperties_ShouldBehaveAsDictionary() + { + // Arrange + var additionalProps = new AdditionalProperties { ["key1"] = "value1", ["key2"] = 123 }; + + // Act + additionalProps["key3"] = true; + + // Assert + Assert.Multiple(() => + { + Assert.That(additionalProps["key1"], Is.EqualTo("value1")); + Assert.That(additionalProps["key2"], Is.EqualTo(123)); + Assert.That((bool)additionalProps["key3"]!, Is.True); + Assert.That(additionalProps.Count, Is.EqualTo(3)); + }); + } + + [Test] + public void AdditionalProperties_ToJsonObject_ShouldSerializeCorrectly() + { + // Arrange + var additionalProps = new AdditionalProperties { ["key1"] = "value1", ["key2"] = 123 }; + + // Act + var jsonObject = additionalProps.ToJsonObject(); + + Assert.Multiple(() => + { + // Assert + Assert.That(jsonObject["key1"]!.GetValue(), Is.EqualTo("value1")); + Assert.That(jsonObject["key2"]!.GetValue(), Is.EqualTo(123)); + }); + } + + [Test] + public void AdditionalProperties_MixReadAndWrite_ShouldOverwriteDeserializedProperty() + { + // Arrange + const string json = """ + { + "id": "1", + "category": "fiction", + "title": "The Hobbit" + } + """; + var record = JsonUtils.Deserialize(json); + + // Act + record.AdditionalProperties["category"] = "non-fiction"; + + // Assert + Assert.Multiple(() => + { + Assert.That(record, Is.Not.Null); + Assert.That(record.Id, Is.EqualTo("1")); + Assert.That(record.AdditionalProperties["category"], Is.EqualTo("non-fiction")); + Assert.That(record.AdditionalProperties["title"], Is.InstanceOf()); + Assert.That( + ((JsonElement)record.AdditionalProperties["title"]!).GetString(), + Is.EqualTo("The Hobbit") + ); + }); + } + + [Test] + public void RecordWithReadonlyAdditionalPropertiesInts_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "extra1": 42, + "extra2": 99 + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.AdditionalProperties["extra1"], Is.EqualTo(42)); + Assert.That(record.AdditionalProperties["extra2"], Is.EqualTo(99)); + }); + } + + [Test] + public void RecordWithAdditionalPropertiesInts_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecordWithInts + { + AdditionalProperties = { ["extra1"] = 42, ["extra2"] = 99 }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.AdditionalProperties["extra1"], Is.EqualTo(42)); + Assert.That(deserializedRecord.AdditionalProperties["extra2"], Is.EqualTo(99)); + }); + } + + [Test] + public void RecordWithReadonlyAdditionalPropertiesDictionaries_OnDeserialized_ShouldPopulateAdditionalProperties() + { + // Arrange + const string json = """ + { + "extra1": { "key1": true, "key2": false }, + "extra2": { "key3": true } + } + """; + + // Act + var record = JsonUtils.Deserialize(json); + + // Assert + Assert.That(record, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(record.AdditionalProperties["extra1"]["key1"], Is.True); + Assert.That(record.AdditionalProperties["extra1"]["key2"], Is.False); + Assert.That(record.AdditionalProperties["extra2"]["key3"], Is.True); + }); + } + + [Test] + public void RecordWithAdditionalPropertiesDictionaries_OnSerialization_ShouldIncludeAdditionalProperties() + { + // Arrange + var record = new WriteableRecordWithDictionaries + { + AdditionalProperties = + { + ["extra1"] = new Dictionary { { "key1", true }, { "key2", false } }, + ["extra2"] = new Dictionary { { "key3", true } }, + }, + }; + + // Act + var json = JsonUtils.Serialize(record); + var deserializedRecord = JsonUtils.Deserialize(json); + + // Assert + Assert.That(deserializedRecord, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(deserializedRecord.AdditionalProperties["extra1"]["key1"], Is.True); + Assert.That(deserializedRecord.AdditionalProperties["extra1"]["key2"], Is.False); + Assert.That(deserializedRecord.AdditionalProperties["extra2"]["key3"], Is.True); + }); + } + + private record Record : IJsonOnDeserialized + { + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecord : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties AdditionalProperties { get; set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } + + private record RecordWithInts : IJsonOnDeserialized + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecordWithInts : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } + + private record RecordWithDictionaries : IJsonOnDeserialized + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public ReadOnlyAdditionalProperties< + Dictionary + > AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + } + + private record WriteableRecordWithDictionaries : IJsonOnDeserialized, IJsonOnSerializing + { + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonIgnore] + public AdditionalProperties> AdditionalProperties { get; } = new(); + + void IJsonOnDeserialized.OnDeserialized() + { + AdditionalProperties.CopyFromExtensionData(_extensionData); + } + + void IJsonOnSerializing.OnSerializing() + { + AdditionalProperties.CopyToExtensionData(_extensionData); + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateOnlyJsonTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateOnlyJsonTests.cs new file mode 100644 index 000000000000..31d92ce6c55e --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateOnlyJsonTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core.Json; + +[TestFixture] +public class DateOnlyJsonTests +{ + [Test] + public void SerializeDateOnly_ShouldMatchExpectedFormat() + { + (DateOnly dateOnly, string expected)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (new DateOnly(2023, 1, 1), "\"2023-01-01\""), + (new DateOnly(2023, 12, 31), "\"2023-12-31\""), + (new DateOnly(2023, 6, 15), "\"2023-06-15\""), + (new DateOnly(2023, 3, 10), "\"2023-03-10\""), + ]; + foreach (var (dateOnly, expected) in testCases) + { + var json = JsonUtils.Serialize(dateOnly); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeDateOnly_ShouldMatchExpectedDateOnly() + { + (DateOnly expected, string json)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (new DateOnly(2023, 1, 1), "\"2023-01-01\""), + (new DateOnly(2023, 12, 31), "\"2023-12-31\""), + (new DateOnly(2023, 6, 15), "\"2023-06-15\""), + (new DateOnly(2023, 3, 10), "\"2023-03-10\""), + ]; + + foreach (var (expected, json) in testCases) + { + var dateOnly = JsonUtils.Deserialize(json); + Assert.That(dateOnly, Is.EqualTo(expected)); + } + } + + [Test] + public void SerializeNullableDateOnly_ShouldMatchExpectedFormat() + { + (DateOnly? dateOnly, string expected)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (null, "null"), + ]; + foreach (var (dateOnly, expected) in testCases) + { + var json = JsonUtils.Serialize(dateOnly); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeNullableDateOnly_ShouldMatchExpectedDateOnly() + { + (DateOnly? expected, string json)[] testCases = + [ + (new DateOnly(2023, 10, 5), "\"2023-10-05\""), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateOnly = JsonUtils.Deserialize(json); + Assert.That(dateOnly, Is.EqualTo(expected)); + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateTimeJsonTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateTimeJsonTests.cs new file mode 100644 index 000000000000..bda587a04362 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/DateTimeJsonTests.cs @@ -0,0 +1,110 @@ +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core.Json; + +[TestFixture] +public class DateTimeJsonTests +{ + [Test] + public void SerializeDateTime_ShouldMatchExpectedFormat() + { + (DateTime dateTime, string expected)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc), "\"2023-01-01T00:00:00.000Z\""), + ( + new DateTime(2023, 12, 31, 23, 59, 59, DateTimeKind.Utc), + "\"2023-12-31T23:59:59.000Z\"" + ), + (new DateTime(2023, 6, 15, 12, 0, 0, DateTimeKind.Utc), "\"2023-06-15T12:00:00.000Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.000Z\"" + ), + ( + new DateTime(2023, 3, 10, 8, 45, 30, 123, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.123Z\"" + ), + ]; + foreach (var (dateTime, expected) in testCases) + { + var json = JsonUtils.Serialize(dateTime); + Assert.That(json, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeDateTime_ShouldMatchExpectedDateTime() + { + (DateTime expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc), "\"2023-01-01T00:00:00.000Z\""), + ( + new DateTime(2023, 12, 31, 23, 59, 59, DateTimeKind.Utc), + "\"2023-12-31T23:59:59.000Z\"" + ), + (new DateTime(2023, 6, 15, 12, 0, 0, DateTimeKind.Utc), "\"2023-06-15T12:00:00.000Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.000Z\"" + ), + (new DateTime(2023, 3, 10, 8, 45, 30, DateTimeKind.Utc), "\"2023-03-10T08:45:30Z\""), + ( + new DateTime(2023, 3, 10, 8, 45, 30, 123, DateTimeKind.Utc), + "\"2023-03-10T08:45:30.123Z\"" + ), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void SerializeNullableDateTime_ShouldMatchExpectedFormat() + { + (DateTime? expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } + + [Test] + public void DeserializeNullableDateTime_ShouldMatchExpectedDateTime() + { + (DateTime? expected, string json)[] testCases = + [ + ( + new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc), + "\"2023-10-05T14:30:00.000Z\"" + ), + (null, "null"), + ]; + + foreach (var (expected, json) in testCases) + { + var dateTime = JsonUtils.Deserialize(json); + Assert.That(dateTime, Is.EqualTo(expected)); + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 000000000000..0ff6a1b14086 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,160 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + + [JsonPropertyName("read_only_nullable_list")] + [JsonAccess(JsonAccessType.ReadOnly)] + public IEnumerable? ReadOnlyNullableList { get; set; } + + [JsonPropertyName("read_only_list")] + [JsonAccess(JsonAccessType.ReadOnly)] + public IEnumerable ReadOnlyList { get; set; } = []; + + [JsonPropertyName("write_only_nullable_list")] + [JsonAccess(JsonAccessType.WriteOnly)] + public IEnumerable? WriteOnlyNullableList { get; set; } + + [JsonPropertyName("write_only_list")] + [JsonAccess(JsonAccessType.WriteOnly)] + public IEnumerable WriteOnlyList { get; set; } = []; + + [JsonPropertyName("normal_list")] + public IEnumerable NormalList { get; set; } = []; + + [JsonPropertyName("normal_nullable_list")] + public IEnumerable? NullableNormalList { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = """ + { + "read_only_prop": "read", + "write_only_prop": "write", + "normal_prop": "normal_prop", + "read_only_nullable_list": ["item1", "item2"], + "read_only_list": ["item3", "item4"], + "write_only_nullable_list": ["item5", "item6"], + "write_only_list": ["item7", "item8"], + "normal_list": ["normal1", "normal2"], + "normal_nullable_list": ["normal1", "normal2"] + } + """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + // String properties + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + + // List properties - read only + var nullableReadOnlyList = obj.ReadOnlyNullableList?.ToArray(); + Assert.That(nullableReadOnlyList, Is.Not.Null); + Assert.That(nullableReadOnlyList, Has.Length.EqualTo(2)); + Assert.That(nullableReadOnlyList![0], Is.EqualTo("item1")); + Assert.That(nullableReadOnlyList![1], Is.EqualTo("item2")); + + var readOnlyList = obj.ReadOnlyList.ToArray(); + Assert.That(readOnlyList, Is.Not.Null); + Assert.That(readOnlyList, Has.Length.EqualTo(2)); + Assert.That(readOnlyList[0], Is.EqualTo("item3")); + Assert.That(readOnlyList[1], Is.EqualTo("item4")); + + // List properties - write only + Assert.That(obj.WriteOnlyNullableList, Is.Null); + Assert.That(obj.WriteOnlyList, Is.Not.Null); + Assert.That(obj.WriteOnlyList, Is.Empty); + + // Normal list property + var normalList = obj.NormalList.ToArray(); + Assert.That(normalList, Is.Not.Null); + Assert.That(normalList, Has.Length.EqualTo(2)); + Assert.That(normalList[0], Is.EqualTo("normal1")); + Assert.That(normalList[1], Is.EqualTo("normal2")); + }); + + // Set up values for serialization + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + obj.WriteOnlyNullableList = new List { "write1", "write2" }; + obj.WriteOnlyList = new List { "write3", "write4" }; + obj.NormalList = new List { "new_normal" }; + obj.NullableNormalList = new List { "new_normal" }; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = """ + { + "write_only_prop": "write", + "normal_prop": "new_value", + "write_only_nullable_list": [ + "write1", + "write2" + ], + "write_only_list": [ + "write3", + "write4" + ], + "normal_list": [ + "new_normal" + ], + "normal_nullable_list": [ + "new_normal" + ] + } + """; + Assert.That(serializedJson, Is.EqualTo(expectedJson).IgnoreWhiteSpace); + } + + [Test] + public void JsonAccessAttribute_WithNullListsInJson_ShouldWorkAsExpected() + { + const string json = """ + { + "read_only_prop": "read", + "normal_prop": "normal_prop", + "read_only_nullable_list": null, + "read_only_list": [] + } + """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + // Read-only nullable list should be null when JSON contains null + var nullableReadOnlyList = obj.ReadOnlyNullableList?.ToArray(); + Assert.That(nullableReadOnlyList, Is.Null); + + // Read-only non-nullable list should never be null, but empty when JSON contains null + var readOnlyList = obj.ReadOnlyList.ToArray(); // This should be initialized to an empty list by default + Assert.That(readOnlyList, Is.Not.Null); + Assert.That(readOnlyList, Is.Empty); + }); + + // Serialize and verify read-only lists are not included + var serializedJson = JsonUtils.Serialize(obj); + Assert.That(serializedJson, Does.Not.Contain("read_only_prop")); + Assert.That(serializedJson, Does.Not.Contain("read_only_nullable_list")); + Assert.That(serializedJson, Does.Not.Contain("read_only_list")); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs new file mode 100644 index 000000000000..f79f10bf40eb --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs @@ -0,0 +1,138 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core.Json; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class StringEnumSerializerTests +{ + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + + private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; + private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); + + private static readonly string JsonWithKnownEnum2 = $$""" + { + "enum_property": "{{KnownEnumValue2}}" + } + """; + + private static readonly string JsonWithUnknownEnum = $$""" + { + "enum_property": "{{UnknownEnumValue}}" + } + """; + + [Test] + public void ShouldParseKnownEnumValue2() + { + var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); + Assert.That(obj, Is.Not.Null); + Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); + } + + [Test] + public void ShouldParseUnknownEnum() + { + var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); + Assert.That(obj, Is.Not.Null); + Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); + } + + [Test] + public void ShouldSerializeKnownEnumValue2() + { + var json = JsonSerializer.SerializeToElement( + new DummyObject { EnumProperty = KnownEnumValue2 }, + JsonOptions + ); + TestContext.Out.WriteLine("Serialized JSON: \n" + json); + var enumString = json.GetProperty("enum_property").GetString(); + Assert.That(enumString, Is.Not.Null); + Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); + } + + [Test] + public void ShouldSerializeUnknownEnum() + { + var json = JsonSerializer.SerializeToElement( + new DummyObject { EnumProperty = UnknownEnumValue }, + JsonOptions + ); + TestContext.Out.WriteLine("Serialized JSON: \n" + json); + var enumString = json.GetProperty("enum_property").GetString(); + Assert.That(enumString, Is.Not.Null); + Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); + } +} + +public class DummyObject +{ + [JsonPropertyName("enum_property")] + public DummyEnum EnumProperty { get; set; } +} + +[JsonConverter(typeof(StringEnumSerializer))] +public readonly record struct DummyEnum : IStringEnum +{ + public DummyEnum(string value) + { + Value = value; + } + + /// + /// The string value of the enum. + /// + public string Value { get; } + + public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); + + public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); + + /// + /// Constant strings for enum values + /// + public static class Values + { + public const string KnownValue1 = "known_value1"; + + public const string KnownValue2 = "known_value2"; + } + + /// + /// Create a string enum with the given value. + /// + public static DummyEnum FromCustom(string value) + { + return new DummyEnum(value); + } + + /// + /// Returns the string value of the enum. + /// + public override string ToString() + { + return Value; + } + + public bool Equals(string? other) + { + return Value.Equals(other); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static explicit operator string(DummyEnum value) => value.Value; + + public static explicit operator DummyEnum(string value) => new(value); + + public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); + + public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringBuilderTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringBuilderTests.cs new file mode 100644 index 000000000000..bd38e8dc9ab9 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringBuilderTests.cs @@ -0,0 +1,560 @@ +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core; + +[TestFixture] +public class QueryStringBuilderTests +{ + [Test] + public void Build_SimpleParameters() + { + var parameters = new List> + { + new("name", "John Doe"), + new("age", "30"), + new("city", "New York"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?name=John%20Doe&age=30&city=New%20York")); + } + + [Test] + public void Build_EmptyList_ReturnsEmptyString() + { + var parameters = new List>(); + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Build_SpecialCharacters() + { + var parameters = new List> + { + new("email", "test@example.com"), + new("url", "https://example.com/path?query=value"), + new("special", "a+b=c&d"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That( + result, + Is.EqualTo( + "?email=test%40example.com&url=https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue&special=a%2Bb%3Dc%26d" + ) + ); + } + + [Test] + public void Build_UnicodeCharacters() + { + var parameters = new List> { new("greeting", "Hello 世界") }; + + var result = QueryStringBuilder.Build(parameters); + + // Verify the Chinese characters are properly UTF-8 encoded + Assert.That(result, Does.StartWith("?greeting=Hello%20")); + Assert.That(result, Does.Contain("%E4%B8%96%E7%95%8C")); // 世界 + } + + [Test] + public void Build_SessionSettings_DeepObject() + { + // Simulate session settings with nested properties + var sessionSettings = new + { + custom_session_id = "my-custom-session-id", + system_prompt = "You are a helpful assistant", + variables = new Dictionary + { + { "userName", "John" }, + { "userAge", 30 }, + { "isPremium", true }, + }, + }; + + // Build query parameters list + var queryParams = new List> { new("api_key", "test_key_123") }; + + // Add session_settings with prefix using the new overload + queryParams.AddRange( + QueryStringConverter.ToDeepObject("session_settings", sessionSettings) + ); + + var result = QueryStringBuilder.Build(queryParams); + + // Verify the result contains properly formatted deep object notation + // Note: Square brackets are URL-encoded as %5B and %5D + Assert.That(result, Does.StartWith("?api_key=test_key_123")); + Assert.That( + result, + Does.Contain("session_settings%5Bcustom_session_id%5D=my-custom-session-id") + ); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20a%20helpful%20assistant") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserName%5D=John")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserAge%5D=30")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BisPremium%5D=true")); + + // Verify it's NOT JSON encoded (no braces or quotes in the original format) + Assert.That(result, Does.Not.Contain("%7B%22")); // Not {" sequence + } + + [Test] + public void Build_ChatApiLikeParameters() + { + // Simulate what ChatApi constructor does + var sessionSettings = new + { + system_prompt = "You are helpful", + variables = new Dictionary { { "name", "Alice" } }, + }; + + var queryParams = new List>(); + + // Simple parameters + var simpleParams = new Dictionary + { + { "access_token", "token123" }, + { "config_id", "config456" }, + { "api_key", "key789" }, + }; + queryParams.AddRange(QueryStringConverter.ToExplodedForm(simpleParams)); + + // Session settings as deep object with prefix + queryParams.AddRange( + QueryStringConverter.ToDeepObject("session_settings", sessionSettings) + ); + + var result = QueryStringBuilder.Build(queryParams); + + // Verify structure (square brackets are URL-encoded) + Assert.That(result, Does.StartWith("?")); + Assert.That(result, Does.Contain("access_token=token123")); + Assert.That(result, Does.Contain("config_id=config456")); + Assert.That(result, Does.Contain("api_key=key789")); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20helpful") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bname%5D=Alice")); + } + + [Test] + public void Build_ReservedCharacters_NotEncoded() + { + var parameters = new List> + { + new("path", "some-path"), + new("id", "123-456_789.test~value"), + }; + + var result = QueryStringBuilder.Build(parameters); + + // Unreserved characters: A-Z a-z 0-9 - _ . ~ + Assert.That(result, Is.EqualTo("?path=some-path&id=123-456_789.test~value")); + } + + [Test] + public void Builder_Add_SimpleParameters() + { + var result = new QueryStringBuilder.Builder() + .Add("name", "John Doe") + .Add("age", 30) + .Add("active", true) + .Build(); + + Assert.That(result, Does.Contain("name=John%20Doe")); + Assert.That(result, Does.Contain("age=30")); + Assert.That(result, Does.Contain("active=true")); + } + + [Test] + public void Builder_Add_NullValuesIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("name", "John") + .Add("middle", null) + .Add("age", 30) + .Build(); + + Assert.That(result, Does.Contain("name=John")); + Assert.That(result, Does.Contain("age=30")); + Assert.That(result, Does.Not.Contain("middle")); + } + + [Test] + public void Builder_AddDeepObject_WithPrefix() + { + var settings = new + { + custom_session_id = "id-123", + system_prompt = "You are helpful", + variables = new { name = "Alice", age = 25 }, + }; + + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddDeepObject("session_settings", settings) + .Build(); + + Assert.That(result, Does.Contain("api_key=key123")); + Assert.That(result, Does.Contain("session_settings%5Bcustom_session_id%5D=id-123")); + Assert.That( + result, + Does.Contain("session_settings%5Bsystem_prompt%5D=You%20are%20helpful") + ); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bname%5D=Alice")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5Bage%5D=25")); + } + + [Test] + public void Builder_AddDeepObject_NullIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddDeepObject("settings", null) + .Build(); + + Assert.That(result, Is.EqualTo("?api_key=key123")); + Assert.That(result, Does.Not.Contain("settings")); + } + + [Test] + public void Builder_AddExploded_WithPrefix() + { + var filter = new { status = "active", type = "user" }; + + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddExploded("filter", filter) + .Build(); + + Assert.That(result, Does.Contain("api_key=key123")); + Assert.That(result, Does.Contain("filter%5Bstatus%5D=active")); + Assert.That(result, Does.Contain("filter%5Btype%5D=user")); + } + + [Test] + public void Builder_AddExploded_NullIgnored() + { + var result = new QueryStringBuilder.Builder() + .Add("api_key", "key123") + .AddExploded("filter", null) + .Build(); + + Assert.That(result, Is.EqualTo("?api_key=key123")); + Assert.That(result, Does.Not.Contain("filter")); + } + + [Test] + public void Builder_WithCapacity() + { + // Test that capacity constructor works without errors + var result = new QueryStringBuilder.Builder(capacity: 10) + .Add("param1", "value1") + .Add("param2", "value2") + .Build(); + + Assert.That(result, Does.Contain("param1=value1")); + Assert.That(result, Does.Contain("param2=value2")); + } + + [Test] + public void Builder_ChatApiLikeUsage() + { + // Simulate real usage from ChatApi + var sessionSettings = new + { + custom_session_id = "session-123", + variables = new Dictionary + { + { "userName", "John" }, + { "userAge", 30 }, + }, + }; + + var result = new QueryStringBuilder.Builder(capacity: 16) + .Add("access_token", "token123") + .Add("allow_connection", true) + .Add("config_id", "config456") + .Add("api_key", "key789") + .AddDeepObject("session_settings", sessionSettings) + .Build(); + + Assert.That(result, Does.StartWith("?")); + Assert.That(result, Does.Contain("access_token=token123")); + Assert.That(result, Does.Contain("allow_connection=true")); + Assert.That(result, Does.Contain("config_id=config456")); + Assert.That(result, Does.Contain("api_key=key789")); + Assert.That(result, Does.Contain("session_settings%5Bcustom_session_id%5D=session-123")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserName%5D=John")); + Assert.That(result, Does.Contain("session_settings%5Bvariables%5D%5BuserAge%5D=30")); + } + + [Test] + public void Builder_EmptyBuilder_ReturnsEmptyString() + { + var result = new QueryStringBuilder.Builder().Build(); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Builder_OnlyNullValues_ReturnsEmptyString() + { + var result = new QueryStringBuilder.Builder() + .Add("param1", null) + .Add("param2", null) + .AddDeepObject("settings", null) + .Build(); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Builder_Set_OverridesSingleValue() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Set("foo", "override") + .Build(); + + Assert.That(result, Is.EqualTo("?foo=override")); + } + + [Test] + public void Builder_Set_OverridesMultipleValues() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "value1") + .Add("foo", "value2") + .Set("foo", "override") + .Build(); + + Assert.That(result, Is.EqualTo("?foo=override")); + } + + [Test] + public void Builder_Set_WithArray_CreatesMultipleParameters() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Set("foo", new[] { "value1", "value2" }) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value1&foo=value2")); + } + + [Test] + public void Builder_Set_WithNull_RemovesParameter() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "original") + .Add("bar", "keep") + .Set("foo", null) + .Build(); + + Assert.That(result, Is.EqualTo("?bar=keep")); + } + + [Test] + public void Builder_MergeAdditional_WithSingleValues() + { + var additional = new List> + { + new("foo", "bar"), + new("baz", "qux"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("existing", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("existing=value")); + Assert.That(result, Does.Contain("foo=bar")); + Assert.That(result, Does.Contain("baz=qux")); + } + + [Test] + public void Builder_MergeAdditional_WithDuplicateKeys_CreatesList() + { + var additional = new List> + { + new("foo", "bar1"), + new("foo", "bar2"), + new("baz", "qux"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("existing", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("existing=value")); + Assert.That(result, Does.Contain("foo=bar1")); + Assert.That(result, Does.Contain("foo=bar2")); + Assert.That(result, Does.Contain("baz=qux")); + } + + [Test] + public void Builder_MergeAdditional_OverridesExistingParameters() + { + var additional = new List> { new("foo", "override") }; + + var result = new QueryStringBuilder.Builder() + .Add("foo", "original1") + .Add("foo", "original2") + .Add("bar", "keep") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("bar=keep")); + Assert.That(result, Does.Contain("foo=override")); + Assert.That(result, Does.Not.Contain("original1")); + Assert.That(result, Does.Not.Contain("original2")); + } + + [Test] + public void Builder_MergeAdditional_WithDuplicates_OverridesExisting() + { + var additional = new List> + { + new("foo", "new1"), + new("foo", "new2"), + new("foo", "new3"), + }; + + var result = new QueryStringBuilder.Builder() + .Add("foo", "original1") + .Add("foo", "original2") + .Add("bar", "keep") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Does.Contain("bar=keep")); + Assert.That(result, Does.Contain("foo=new1")); + Assert.That(result, Does.Contain("foo=new2")); + Assert.That(result, Does.Contain("foo=new3")); + Assert.That(result, Does.Not.Contain("original1")); + Assert.That(result, Does.Not.Contain("original2")); + } + + [Test] + public void Builder_MergeAdditional_WithNull_NoOp() + { + var result = new QueryStringBuilder.Builder() + .Add("foo", "value") + .MergeAdditional(null) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value")); + } + + [Test] + public void Builder_MergeAdditional_WithEmptyList_NoOp() + { + var additional = new List>(); + + var result = new QueryStringBuilder.Builder() + .Add("foo", "value") + .MergeAdditional(additional) + .Build(); + + Assert.That(result, Is.EqualTo("?foo=value")); + } + + [Test] + public void Builder_MergeAdditional_RealWorldScenario() + { + // SDK generates foo=foo1&foo=foo2 + var builder = new QueryStringBuilder.Builder() + .Add("foo", "foo1") + .Add("foo", "foo2") + .Add("bar", "baz"); + + // User provides foo=override in AdditionalQueryParameters + var additional = new List> { new("foo", "override") }; + + var result = builder.MergeAdditional(additional).Build(); + + // Result should be foo=override&bar=baz (user overrides SDK) + Assert.That(result, Does.Contain("bar=baz")); + Assert.That(result, Does.Contain("foo=override")); + Assert.That(result, Does.Not.Contain("foo1")); + Assert.That(result, Does.Not.Contain("foo2")); + } + + [Test] + public void Builder_MergeAdditional_UserProvidesMultipleValues() + { + // SDK generates no foo parameter + var builder = new QueryStringBuilder.Builder().Add("bar", "baz"); + + // User provides foo=bar1&foo=bar2 in AdditionalQueryParameters + var additional = new List> + { + new("foo", "bar1"), + new("foo", "bar2"), + }; + + var result = builder.MergeAdditional(additional).Build(); + + // Result should be bar=baz&foo=bar1&foo=bar2 + Assert.That(result, Does.Contain("bar=baz")); + Assert.That(result, Does.Contain("foo=bar1")); + Assert.That(result, Does.Contain("foo=bar2")); + } + + [Test] + public void Builder_Add_WithCollection_CreatesMultipleParameters() + { + var tags = new[] { "tag1", "tag2", "tag3" }; + var result = new QueryStringBuilder.Builder().Add("tag", tags).Build(); + + Assert.That(result, Does.Contain("tag=tag1")); + Assert.That(result, Does.Contain("tag=tag2")); + Assert.That(result, Does.Contain("tag=tag3")); + } + + [Test] + public void Builder_Add_WithList_CreatesMultipleParameters() + { + var ids = new List { 1, 2, 3 }; + var result = new QueryStringBuilder.Builder().Add("id", ids).Build(); + + Assert.That(result, Does.Contain("id=1")); + Assert.That(result, Does.Contain("id=2")); + Assert.That(result, Does.Contain("id=3")); + } + + [Test] + public void Builder_Set_WithCollection_ReplacesAllPreviousValues() + { + var result = new QueryStringBuilder.Builder() + .Add("id", 1) + .Add("id", 2) + .Set("id", new[] { 10, 20, 30 }) + .Build(); + + Assert.That(result, Does.Contain("id=10")); + Assert.That(result, Does.Contain("id=20")); + Assert.That(result, Does.Contain("id=30")); + // Check that old values are not present (use word boundaries to avoid false positives with id=10) + Assert.That(result, Does.Not.Contain("id=1&")); + Assert.That(result, Does.Not.Contain("id=2&")); + Assert.That(result, Does.Not.Contain("id=1?")); + Assert.That(result, Does.Not.Contain("id=2?")); + Assert.That(result, Does.Not.EndWith("id=1")); + Assert.That(result, Does.Not.EndWith("id=2")); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringConverterTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringConverterTests.cs new file mode 100644 index 000000000000..867588b0b792 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/QueryStringConverterTests.cs @@ -0,0 +1,158 @@ +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core; + +[TestFixture] +public class QueryStringConverterTests +{ + [Test] + public void ToQueryStringCollection_Form() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToForm(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates]", "39.78172,-89.65015"), + new("Tags", "Developer,Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_ExplodedForm() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToExplodedForm(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates]", "39.78172"), + new("Address[Coordinates]", "-89.65015"), + new("Tags", "Developer"), + new("Tags", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_DeepObject() + { + var obj = new + { + Name = "John", + Age = 30, + Address = new + { + Street = "123 Main St", + City = "Anytown", + Coordinates = new[] { 39.781721f, -89.650148f }, + }, + Tags = new[] { "Developer", "Blogger" }, + }; + var result = QueryStringConverter.ToDeepObject(obj); + var expected = new List> + { + new("Name", "John"), + new("Age", "30"), + new("Address[Street]", "123 Main St"), + new("Address[City]", "Anytown"), + new("Address[Coordinates][0]", "39.78172"), + new("Address[Coordinates][1]", "-89.65015"), + new("Tags[0]", "Developer"), + new("Tags[1]", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_OnString_ThrowsException() + { + var exception = Assert.Throws(() => + QueryStringConverter.ToForm("invalid") + ); + Assert.That( + exception.Message, + Is.EqualTo( + "Only objects can be converted to query string collections. Given type is String." + ) + ); + } + + [Test] + public void ToQueryStringCollection_OnArray_ThrowsException() + { + var exception = Assert.Throws(() => + QueryStringConverter.ToForm(Array.Empty()) + ); + Assert.That( + exception.Message, + Is.EqualTo( + "Only objects can be converted to query string collections. Given type is Array." + ) + ); + } + + [Test] + public void ToQueryStringCollection_DeepObject_WithPrefix() + { + var obj = new + { + custom_session_id = "my-id", + system_prompt = "You are helpful", + variables = new { name = "Alice", age = 25 }, + }; + var result = QueryStringConverter.ToDeepObject("session_settings", obj); + var expected = new List> + { + new("session_settings[custom_session_id]", "my-id"), + new("session_settings[system_prompt]", "You are helpful"), + new("session_settings[variables][name]", "Alice"), + new("session_settings[variables][age]", "25"), + }; + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void ToQueryStringCollection_ExplodedForm_WithPrefix() + { + var obj = new { Name = "John", Tags = new[] { "Developer", "Blogger" } }; + var result = QueryStringConverter.ToExplodedForm("user", obj); + var expected = new List> + { + new("user[Name]", "John"), + new("user[Tags]", "Developer"), + new("user[Tags]", "Blogger"), + }; + Assert.That(result, Is.EqualTo(expected)); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/MultipartFormTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/MultipartFormTests.cs new file mode 100644 index 000000000000..faa8bf30ffab --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/MultipartFormTests.cs @@ -0,0 +1,1121 @@ +using global::System.Net.Http; +using global::System.Text; +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSimpleApi.Core; +using SystemTask = global::System.Threading.Tasks.Task; + +namespace SeedSimpleApi.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class MultipartFormTests +{ + private static SimpleObject _simpleObject = new(); + + private static string _simpleFormEncoded = + "meta=data&Date=2023-10-01&Time=12:00:00&Duration=01:00:00&Id=1a1bb98f-47c6-407b-9481-78476affe52a&IsActive=true&Count=42&Initial=A&Values=data,2023-10-01,12:00:00,01:00:00,1a1bb98f-47c6-407b-9481-78476affe52a,true,42,A"; + + private static string _simpleExplodedFormEncoded = + "meta=data&Date=2023-10-01&Time=12:00:00&Duration=01:00:00&Id=1a1bb98f-47c6-407b-9481-78476affe52a&IsActive=true&Count=42&Initial=A&Values=data&Values=2023-10-01&Values=12:00:00&Values=01:00:00&Values=1a1bb98f-47c6-407b-9481-78476affe52a&Values=true&Values=42&Values=A"; + + private static ComplexObject _complexObject = new(); + + private static string _complexJson = """ + { + "meta": "data", + "Nested": { + "foo": "value" + }, + "NestedDictionary": { + "key": { + "foo": "value" + } + }, + "ListOfObjects": [ + { + "foo": "value" + }, + { + "foo": "value2" + } + ], + "Date": "2023-10-01", + "Time": "12:00:00", + "Duration": "01:00:00", + "Id": "1a1bb98f-47c6-407b-9481-78476affe52a", + "IsActive": true, + "Count": 42, + "Initial": "A" + } + """; + + [Test] + public async SystemTask ShouldAddStringPart() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, partInput]); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddStringPart() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", null); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithNullsInList() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, null, partInput]); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringPart_WithContentType() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput, "text/xml"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringPart_WithContentTypeAndCharset() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringPart("string", partInput, "text/xml; charset=utf-8"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=string + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithContentType() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts("strings", [partInput, partInput], "text/xml"); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/xml + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddStringParts_WithContentTypeAndCharset() + { + const string partInput = "string content"; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddStringParts( + "strings", + [partInput, partInput], + "text/xml; charset=utf-8" + ); + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary} + Content-Type: text/xml; charset=utf-8 + Content-Disposition: form-data; name=strings + + {partInput} + --{boundary}-- + """; + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFileName() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithoutFileName() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", partInput); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithContentType() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter + { + Stream = partInput, + FileName = "test.txt", + ContentType = "text/plain", + }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "ignored-fallback-content-type"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithContentTypeAndCharset() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter + { + Stream = partInput, + FileName = "test.txt", + ContentType = "text/plain; charset=utf-8", + }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "ignored-fallback-content-type"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFallbackContentType() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "text/plain"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameter_WithFallbackContentTypeAndCharset() + { + var (partInput, partExpectedString) = GetFileParameterTestData(); + var file = new FileParameter { Stream = partInput, FileName = "test.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", file, "text/plain; charset=utf-8"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt + + {partExpectedString} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameters() + { + var (partInput1, partExpectedString1) = GetFileParameterTestData(); + var (partInput2, partExpectedString2) = GetFileParameterTestData(); + var file1 = new FileParameter { Stream = partInput1, FileName = "test1.txt" }; + var file2 = new FileParameter { Stream = partInput2, FileName = "test2.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterParts("file", [file1, file2]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test1.txt; filename*=utf-8''test1.txt + + {partExpectedString1} + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test2.txt; filename*=utf-8''test2.txt + + {partExpectedString2} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFileParameters_WithNullsInList() + { + var (partInput1, partExpectedString1) = GetFileParameterTestData(); + var (partInput2, partExpectedString2) = GetFileParameterTestData(); + var file1 = new FileParameter { Stream = partInput1, FileName = "test1.txt" }; + var file2 = new FileParameter { Stream = partInput2, FileName = "test2.txt" }; + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterParts("file", [file1, null, file2]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test1.txt; filename*=utf-8''test1.txt + + {partExpectedString1} + --{boundary} + Content-Type: application/octet-stream + Content-Disposition: form-data; name=file; filename=test2.txt; filename*=utf-8''test2.txt + + {partExpectedString2} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddFileParameter() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFileParameterPart("file", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonPart_WithComplexObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonPart("object", _complexObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=object + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonPart_WithComplexObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [_complexObject, _complexObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask GivenNull_ShouldNotAddJsonPart() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonPart("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [_complexObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/json + Content-Disposition: form-data; name=objects + + {_complexJson} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddJsonParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddJsonParts("objects", [new { }], "application/json-patch+json"); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $$""" + --{{boundary}} + Content-Type: application/json-patch+json + Content-Disposition: form-data; name=objects + + {} + --{{boundary}}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithSimpleObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart("object", _simpleObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=object + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithSimpleObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("objects", [_simpleObject, _simpleObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddFormEncodedParts_WithNull() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddFormEncodedParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts("objects", [_simpleObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedPart_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedPart_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddFormEncodedParts_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithSimpleObject() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart("object", _simpleObject); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=object + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithSimpleObjectList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts("objects", [_simpleObject, _simpleObject]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddExplodedFormEncodedParts_WithNull() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart("object", null); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldNotAddExplodedFormEncodedParts_WithNullsInList() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts("objects", [_simpleObject, null]); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + {EscapeFormEncodedString(_simpleExplodedFormEncoded)} + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedPart_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedPart_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedPart( + "objects", + new { foo = "bar" }, + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithContentType() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + [Test] + public async SystemTask ShouldAddExplodedFormEncodedParts_WithContentTypeAndCharset() + { + var multipartFormRequest = CreateMultipartFormRequest(); + multipartFormRequest.AddExplodedFormEncodedParts( + "objects", + [new { foo = "bar" }], + "application/x-www-form-urlencoded; charset=utf-8" + ); + + var httpContent = multipartFormRequest.CreateContent(); + Assert.That(httpContent, Is.InstanceOf()); + var multipartContent = (MultipartFormDataContent)httpContent; + + var boundary = GetBoundary(multipartContent); + var expected = $""" + --{boundary} + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Disposition: form-data; name=objects + + foo=bar + --{boundary}-- + """; + + var actual = await multipartContent.ReadAsStringAsync(); + Assert.That(actual, Is.EqualTo(expected).IgnoreWhiteSpace); + } + + private static string EscapeFormEncodedString(string input) + { + return string.Join( + "&", + input + .Split('&') + .Select(x => x.Split('=')) + .Select(x => $"{Uri.EscapeDataString(x[0])}={Uri.EscapeDataString(x[1])}") + ); + } + + private static string GetBoundary(MultipartFormDataContent content) + { + return content + .Headers.ContentType?.Parameters.Single(p => + p.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase) + ) + .Value?.Trim('"') + ?? throw new global::System.Exception("Boundary not found"); + } + + private static SeedSimpleApi.Core.MultipartFormRequest CreateMultipartFormRequest() + { + return new SeedSimpleApi.Core.MultipartFormRequest + { + BaseUrl = "https://localhost", + Method = HttpMethod.Post, + Path = "", + }; + } + + private static (Stream partInput, string partExpectedString) GetFileParameterTestData() + { + const string partExpectedString = "file content"; + var partInput = new MemoryStream(Encoding.Default.GetBytes(partExpectedString)); + return (partInput, partExpectedString); + } + + private class SimpleObject + { + [JsonPropertyName("meta")] + public string Meta { get; set; } = "data"; + public DateOnly Date { get; set; } = DateOnly.Parse("2023-10-01"); + public TimeOnly Time { get; set; } = TimeOnly.Parse("12:00:00"); + public TimeSpan Duration { get; set; } = TimeSpan.FromHours(1); + public Guid Id { get; set; } = Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"); + public bool IsActive { get; set; } = true; + public int Count { get; set; } = 42; + public char Initial { get; set; } = 'A'; + public IEnumerable Values { get; set; } = + [ + "data", + DateOnly.Parse("2023-10-01"), + TimeOnly.Parse("12:00:00"), + TimeSpan.FromHours(1), + Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"), + true, + 42, + 'A', + ]; + } + + private class ComplexObject + { + [JsonPropertyName("meta")] + public string Meta { get; set; } = "data"; + + public object Nested { get; set; } = new { foo = "value" }; + + public Dictionary NestedDictionary { get; set; } = + new() { { "key", new { foo = "value" } } }; + + public IEnumerable ListOfObjects { get; set; } = + new List { new { foo = "value" }, new { foo = "value2" } }; + + public DateOnly Date { get; set; } = DateOnly.Parse("2023-10-01"); + public TimeOnly Time { get; set; } = TimeOnly.Parse("12:00:00"); + public TimeSpan Duration { get; set; } = TimeSpan.FromHours(1); + public Guid Id { get; set; } = Guid.Parse("1a1bb98f-47c6-407b-9481-78476affe52a"); + public bool IsActive { get; set; } = true; + public int Count { get; set; } = 42; + public char Initial { get; set; } = 'A'; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/QueryParameterTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/QueryParameterTests.cs new file mode 100644 index 000000000000..35c04a0acbe7 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/QueryParameterTests.cs @@ -0,0 +1,108 @@ +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class QueryParameterTests +{ + [Test] + public void QueryParameters_BasicParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .Add("baz", "qux") + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar&baz=qux")); + } + + [Test] + public void QueryParameters_SpecialCharacterEscaping() + { + var queryString = new QueryStringBuilder.Builder() + .Add("email", "bob+test@example.com") + .Add("%Complete", "100") + .Add("space test", "hello world") + .Build(); + + Assert.That(queryString, Does.Contain("email=bob%2Btest%40example.com")); + Assert.That(queryString, Does.Contain("%25Complete=100")); + Assert.That(queryString, Does.Contain("space%20test=hello%20world")); + } + + [Test] + public void QueryParameters_MergeAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("sdk", "param") + .MergeAdditional(new List> { new("user", "value") }) + .Build(); + + Assert.That(queryString, Does.Contain("sdk=param")); + Assert.That(queryString, Does.Contain("user=value")); + } + + [Test] + public void QueryParameters_AdditionalOverridesSdk() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "sdk_value") + .MergeAdditional(new List> { new("foo", "user_override") }) + .Build(); + + Assert.That(queryString, Does.Contain("foo=user_override")); + Assert.That(queryString, Does.Not.Contain("sdk_value")); + } + + [Test] + public void QueryParameters_AdditionalMultipleValues() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "sdk_value") + .MergeAdditional( + new List> { new("foo", "user1"), new("foo", "user2") } + ) + .Build(); + + Assert.That(queryString, Does.Contain("foo=user1")); + Assert.That(queryString, Does.Contain("foo=user2")); + Assert.That(queryString, Does.Not.Contain("sdk_value")); + } + + [Test] + public void QueryParameters_OnlyAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .MergeAdditional( + new List> { new("foo", "bar"), new("baz", "qux") } + ) + .Build(); + + Assert.That(queryString, Does.Contain("foo=bar")); + Assert.That(queryString, Does.Contain("baz=qux")); + } + + [Test] + public void QueryParameters_EmptyAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .MergeAdditional(new List>()) + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar")); + } + + [Test] + public void QueryParameters_NullAdditionalParameters() + { + var queryString = new QueryStringBuilder.Builder() + .Add("foo", "bar") + .MergeAdditional(null) + .Build(); + + Assert.That(queryString, Is.EqualTo("?foo=bar")); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs new file mode 100644 index 000000000000..0c9ef9701755 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs @@ -0,0 +1,405 @@ +using global::System.Net.Http; +using NUnit.Framework; +using SeedSimpleApi.Core; +using WireMock.Server; +using SystemTask = global::System.Threading.Tasks.Task; +using WireMockRequest = WireMock.RequestBuilders.Request; +using WireMockResponse = WireMock.ResponseBuilders.Response; + +namespace SeedSimpleApi.Test.Core.RawClientTests; + +[TestFixture] +[Parallelizable(ParallelScope.Self)] +public class RetriesTests +{ + private const int MaxRetries = 3; + private WireMockServer _server; + private HttpClient _httpClient; + private RawClient _rawClient; + private string _baseUrl; + + [SetUp] + public void SetUp() + { + _server = WireMockServer.Start(); + _baseUrl = _server.Url ?? ""; + _httpClient = new HttpClient { BaseAddress = new Uri(_baseUrl) }; + _rawClient = new RawClient( + new ClientOptions { HttpClient = _httpClient, MaxRetries = MaxRetries } + ) + { + BaseRetryDelay = 0, + }; + } + + [Test] + [TestCase(408)] + [TestCase(429)] + [TestCase(500)] + [TestCase(504)] + public async SystemTask SendRequestAsync_ShouldRetry_OnRetryableStatusCodes(int statusCode) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Server Error") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + + Assert.That(_server.LogEntries, Has.Count.EqualTo(MaxRetries)); + } + } + + [Test] + [TestCase(400)] + [TestCase(409)] + public async SystemTask SendRequestAsync_ShouldRetry_OnNonRetryableStatusCodes(int statusCode) + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(statusCode).WithBody("Failure")); + + var request = new JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + Body = new { }, + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(statusCode)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldNotRetry_WithStreamRequest() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429).WithBody("Failure")); + + var request = new StreamRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new MemoryStream(), + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(429)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldNotRetry_WithMultiPartFormRequest_WithStream() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429).WithBody("Failure")); + + var request = new SeedSimpleApi.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddFileParameterPart("file", new MemoryStream()); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(429)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Failure")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(1)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRetry_WithMultiPartFormRequest_WithoutStream() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WillSetStateTo("Server Error") + .RespondWith(WireMockResponse.Create().WithStatusCode(429)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WhenStateIs("Server Error") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(429)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("Retry") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedSimpleApi.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddJsonPart("object", new { }); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(MaxRetries)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectRetryAfterHeader_WithSecondsValue() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfter") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse.Create().WithStatusCode(429).WithHeader("Retry-After", "1") + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfter") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectRetryAfterHeader_WithHttpDateValue() + { + var retryAfterDate = DateTimeOffset.UtcNow.AddSeconds(1).ToString("R"); + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfterDate") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse + .Create() + .WithStatusCode(429) + .WithHeader("Retry-After", retryAfterDate) + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RetryAfterDate") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() + { + var resetTime = DateTimeOffset.UtcNow.AddSeconds(1).ToUnixTimeSeconds().ToString(); + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RateLimitReset") + .WillSetStateTo("Success") + .RespondWith( + WireMockResponse + .Create() + .WithStatusCode(429) + .WithHeader("X-RateLimit-Reset", resetTime) + ); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingGet()) + .InScenario("RateLimitReset") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new EmptyRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Get, + Path = "/test", + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + Assert.Multiple(() => + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + }); + } + + [Test] + public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() + { + const string expectedBody = """{"key":"value"}"""; + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryWithBody") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryWithBody") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new JsonRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + Body = new { key = "value" }, + }; + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + + // Verify the retried request preserved the JSON body + var retriedEntry = _server.LogEntries.ElementAt(1); + Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + } + } + + [Test] + public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() + { + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryMultipart") + .WillSetStateTo("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(500)); + + _server + .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) + .InScenario("RetryMultipart") + .WhenStateIs("Success") + .RespondWith(WireMockResponse.Create().WithStatusCode(200).WithBody("Success")); + + var request = new SeedSimpleApi.Core.MultipartFormRequest + { + BaseUrl = _baseUrl, + Method = HttpMethod.Post, + Path = "/test", + }; + request.AddJsonPart("object", new { key = "value" }); + + var response = await _rawClient.SendRequestAsync(request); + Assert.That(response.StatusCode, Is.EqualTo(200)); + + var content = await response.Raw.Content.ReadAsStringAsync(); + using (Assert.EnterMultipleScope()) + { + Assert.That(content, Is.EqualTo("Success")); + Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); + + // Verify the retried request preserved the multipart body + var retriedEntry = _server.LogEntries.ElementAt(1); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + } + } + + [TearDown] + public void TearDown() + { + _server.Dispose(); + _httpClient.Dispose(); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/WithRawResponseTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/WithRawResponseTests.cs new file mode 100644 index 000000000000..60b3fd7e9df6 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/WithRawResponseTests.cs @@ -0,0 +1,269 @@ +using global::System.Net; +using global::System.Net.Http.Headers; +using NUnit.Framework; +using SeedSimpleApi; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Core; + +[TestFixture] +public class WithRawResponseTests +{ + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_DirectAwait_ReturnsData() + { + // Arrange + var expectedData = "test-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act + var result = await task; + + // Assert + Assert.That(result, Is.EqualTo(expectedData)); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_WithRawResponse_ReturnsDataAndMetadata() + { + // Arrange + var expectedData = "test-data"; + var expectedStatusCode = HttpStatusCode.Created; + var task = CreateWithRawResponseTask(expectedData, expectedStatusCode); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.Data, Is.EqualTo(expectedData)); + Assert.That(result.RawResponse.StatusCode, Is.EqualTo(expectedStatusCode)); + Assert.That(result.RawResponse.Url, Is.Not.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValue_CaseInsensitive() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Request-Id", "12345"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act & Assert + Assert.That(headers.TryGetValue("X-Request-Id", out var value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + + Assert.That(headers.TryGetValue("x-request-id", out value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + + Assert.That(headers.TryGetValue("X-REQUEST-ID", out value), Is.True); + Assert.That(value, Is.EqualTo("12345")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValues_ReturnsMultipleValues() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("Set-Cookie", new[] { "cookie1=value1", "cookie2=value2" }); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValues("Set-Cookie", out var values); + + // Assert + Assert.That(success, Is.True); + Assert.That(values, Is.Not.Null); + Assert.That(values!.Count(), Is.EqualTo(2)); + Assert.That(values, Does.Contain("cookie1=value1")); + Assert.That(values, Does.Contain("cookie2=value2")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_ContentType_ReturnsValue() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Content = new StringContent( + "{}", + global::System.Text.Encoding.UTF8, + "application/json" + ); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var contentType = headers.ContentType; + + // Assert + Assert.That(contentType, Is.Not.Null); + Assert.That(contentType, Does.Contain("application/json")); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_ContentLength_ReturnsValue() + { + // Arrange + var content = "test content"; + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Content = new StringContent(content); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var contentLength = headers.ContentLength; + + // Assert + Assert.That(contentLength, Is.Not.Null); + Assert.That(contentLength, Is.GreaterThan(0)); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_Contains_ReturnsTrueForExistingHeader() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Custom-Header", "value"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act & Assert + Assert.That(headers.Contains("X-Custom-Header"), Is.True); + Assert.That(headers.Contains("x-custom-header"), Is.True); + Assert.That(headers.Contains("NonExistent"), Is.False); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_Enumeration_IncludesAllHeaders() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + response.Headers.Add("X-Header-1", "value1"); + response.Headers.Add("X-Header-2", "value2"); + response.Content = new StringContent("test"); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var allHeaders = headers.ToList(); + + // Assert + Assert.That(allHeaders.Count, Is.GreaterThan(0)); + Assert.That(allHeaders.Any(h => h.Name == "X-Header-1"), Is.True); + Assert.That(allHeaders.Any(h => h.Name == "X-Header-2"), Is.True); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_ErrorStatusCode_StillReturnsMetadata() + { + // Arrange + var expectedData = "error-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.BadRequest); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.Data, Is.EqualTo(expectedData)); + Assert.That(result.RawResponse.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_Url_IsPreserved() + { + // Arrange + var expectedUrl = new Uri("https://api.example.com/users/123"); + var task = CreateWithRawResponseTask("data", HttpStatusCode.OK, expectedUrl); + + // Act + var result = await task.WithRawResponse(); + + // Assert + Assert.That(result.RawResponse.Url, Is.EqualTo(expectedUrl)); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValue_NonExistentHeader_ReturnsFalse() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValue("X-NonExistent", out var value); + + // Assert + Assert.That(success, Is.False); + Assert.That(value, Is.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task ResponseHeaders_TryGetValues_NonExistentHeader_ReturnsFalse() + { + // Arrange + using var response = CreateHttpResponse(HttpStatusCode.OK); + var headers = ResponseHeaders.FromHttpResponseMessage(response); + + // Act + var success = headers.TryGetValues("X-NonExistent", out var values); + + // Assert + Assert.That(success, Is.False); + Assert.That(values, Is.Null); + } + + [Test] + public async global::System.Threading.Tasks.Task WithRawResponseTask_ImplicitConversion_ToTask() + { + // Arrange + var expectedData = "test-data"; + var task = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act - implicitly convert to Task + global::System.Threading.Tasks.Task regularTask = task; + var result = await regularTask; + + // Assert + Assert.That(result, Is.EqualTo(expectedData)); + } + + [Test] + public void WithRawResponseTask_ImplicitConversion_AssignToTaskVariable() + { + // Arrange + var expectedData = "test-data"; + var wrappedTask = CreateWithRawResponseTask(expectedData, HttpStatusCode.OK); + + // Act - assign to Task variable + global::System.Threading.Tasks.Task regularTask = wrappedTask; + + // Assert + Assert.That(regularTask, Is.Not.Null); + Assert.That(regularTask, Is.InstanceOf>()); + } + + // Helper methods + + private static WithRawResponseTask CreateWithRawResponseTask( + T data, + HttpStatusCode statusCode, + Uri? url = null + ) + { + url ??= new Uri("https://api.example.com/test"); + using var httpResponse = CreateHttpResponse(statusCode); + httpResponse.RequestMessage = new HttpRequestMessage(HttpMethod.Get, url); + + var rawResponse = new RawResponse + { + StatusCode = statusCode, + Url = url, + Headers = ResponseHeaders.FromHttpResponseMessage(httpResponse), + }; + + var withRawResponse = new WithRawResponse { Data = data, RawResponse = rawResponse }; + + var task = global::System.Threading.Tasks.Task.FromResult(withRawResponse); + return new WithRawResponseTask(task); + } + + private static HttpResponseMessage CreateHttpResponse(HttpStatusCode statusCode) + { + return new HttpResponseMessage(statusCode) { Content = new StringContent("") }; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.Custom.props b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.Custom.props new file mode 100644 index 000000000000..aac9b5020d80 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.Custom.props @@ -0,0 +1,6 @@ + + diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.csproj b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.csproj new file mode 100644 index 000000000000..4a044b376ba3 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/SeedSimpleApi.Test.csproj @@ -0,0 +1,39 @@ + + + net8.0 + 12 + enable + enable + false + true + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/TestClient.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/TestClient.cs new file mode 100644 index 000000000000..ae9e93c48982 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/TestClient.cs @@ -0,0 +1,6 @@ +using NUnit.Framework; + +namespace SeedSimpleApi.Test; + +[TestFixture] +public class TestClient; diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs new file mode 100644 index 000000000000..d2feea66b1b3 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using SeedSimpleApi; +using WireMock.Logging; +using WireMock.Server; +using WireMock.Settings; + +namespace SeedSimpleApi.Test.Unit.MockServer; + +[SetUpFixture] +public class BaseMockServerTest +{ + protected static WireMockServer Server { get; set; } = null!; + + protected static SeedSimpleApiClient Client { get; set; } = null!; + + protected static RequestOptions RequestOptions { get; set; } = new(); + + [OneTimeSetUp] + public void GlobalSetup() + { + // Start the WireMock server + Server = WireMockServer.Start( + new WireMockServerSettings { Logger = new WireMockConsoleLogger() } + ); + + // Initialize the Client + Client = new SeedSimpleApiClient( + "TOKEN", + clientOptions: new ClientOptions { BaseUrl = Server.Urls[0], MaxRetries = 0 } + ); + } + + [OneTimeTearDown] + public void GlobalTeardown() + { + Server.Stop(); + Server.Dispose(); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs new file mode 100644 index 000000000000..78425be96816 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs @@ -0,0 +1,33 @@ +using NUnit.Framework; +using SeedSimpleApi.Test.Unit.MockServer; +using SeedSimpleApi.Test.Utils; + +namespace SeedSimpleApi.Test.Unit.MockServer.User; + +[TestFixture] +public class GetTest : BaseMockServerTest +{ + [NUnit.Framework.Test] + public async Task MockServerTest() + { + const string mockResponse = """ + { + "id": "id", + "name": "name", + "email": "email" + } + """; + + Server + .Given(WireMock.RequestBuilders.Request.Create().WithPath("/users/id").UsingGet()) + .RespondWith( + WireMock + .ResponseBuilders.Response.Create() + .WithStatusCode(200) + .WithBody(mockResponse) + ); + + var response = await Client.User.GetAsync("id"); + JsonAssert.AreEqual(response, mockResponse); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/AdditionalPropertiesComparer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/AdditionalPropertiesComparer.cs new file mode 100644 index 000000000000..78b00f5801ee --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/AdditionalPropertiesComparer.cs @@ -0,0 +1,126 @@ +using System.Text.Json; +using NUnit.Framework.Constraints; +using SeedSimpleApi; +using SeedSimpleApi.Core; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle AdditionalProperties values. +/// +public static class AdditionalPropertiesComparerExtensions +{ + /// + /// Modifies the EqualConstraint to handle AdditionalProperties instances by comparing their + /// serialized JSON representations. This handles the type mismatch between native C# types + /// and JsonElement values that occur when comparing manually constructed objects with + /// deserialized objects. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingAdditionalPropertiesComparer(this EqualConstraint constraint) + { + constraint.Using( + (x, y) => + { + if (x.Count != y.Count) + { + return false; + } + + foreach (var key in x.Keys) + { + if (!y.ContainsKey(key)) + { + return false; + } + + var xElement = JsonUtils.SerializeToElement(x[key]); + var yElement = JsonUtils.SerializeToElement(y[key]); + + if (!JsonElementsAreEqual(xElement, yElement)) + { + return false; + } + } + + return true; + } + ); + + return constraint; + } + + private static bool JsonElementsAreEqual(JsonElement x, JsonElement y) + { + if (x.ValueKind != y.ValueKind) + { + return false; + } + + return x.ValueKind switch + { + JsonValueKind.Object => CompareJsonObjects(x, y), + JsonValueKind.Array => CompareJsonArrays(x, y), + JsonValueKind.String => x.GetString() == y.GetString(), + JsonValueKind.Number => x.GetDecimal() == y.GetDecimal(), + JsonValueKind.True => true, + JsonValueKind.False => true, + JsonValueKind.Null => true, + _ => false, + }; + } + + private static bool CompareJsonObjects(JsonElement x, JsonElement y) + { + var xProps = new Dictionary(); + var yProps = new Dictionary(); + + foreach (var prop in x.EnumerateObject()) + xProps[prop.Name] = prop.Value; + + foreach (var prop in y.EnumerateObject()) + yProps[prop.Name] = prop.Value; + + if (xProps.Count != yProps.Count) + { + return false; + } + + foreach (var key in xProps.Keys) + { + if (!yProps.ContainsKey(key)) + { + return false; + } + + if (!JsonElementsAreEqual(xProps[key], yProps[key])) + { + return false; + } + } + + return true; + } + + private static bool CompareJsonArrays(JsonElement x, JsonElement y) + { + var xArray = x.EnumerateArray().ToList(); + var yArray = y.EnumerateArray().ToList(); + + if (xArray.Count != yArray.Count) + { + return false; + } + + for (var i = 0; i < xArray.Count; i++) + { + if (!JsonElementsAreEqual(xArray[i], yArray[i])) + { + return false; + } + } + + return true; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonAssert.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonAssert.cs new file mode 100644 index 000000000000..de3d0e6f2daf --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonAssert.cs @@ -0,0 +1,19 @@ +using global::System.Text.Json; +using NUnit.Framework; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi.Test.Utils; + +internal static class JsonAssert +{ + /// + /// Asserts that the serialized JSON of an object equals the expected JSON string. + /// Uses JsonElement comparison for reliable deep equality of collections and union types. + /// + internal static void AreEqual(object actual, string expectedJson) + { + var actualElement = JsonUtils.SerializeToElement(actual); + var expectedElement = JsonUtils.Deserialize(expectedJson); + Assert.That(actualElement, Is.EqualTo(expectedElement).UsingJsonElementComparer()); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonElementComparer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonElementComparer.cs new file mode 100644 index 000000000000..a37ef402c1ac --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/JsonElementComparer.cs @@ -0,0 +1,236 @@ +using global::System.Text.Json; +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle JsonElement objects. +/// +public static class JsonElementComparerExtensions +{ + /// + /// Extension method for comparing JsonElement objects in NUnit tests. + /// Property order doesn't matter, but array order does matter. + /// Includes special handling for DateTime string formats. + /// + /// The Is.EqualTo() constraint instance. + /// A constraint that can compare JsonElements with detailed diffs. + public static EqualConstraint UsingJsonElementComparer(this EqualConstraint constraint) + { + return constraint.Using(new JsonElementComparer()); + } +} + +/// +/// Equality comparer for JsonElement with detailed reporting. +/// Property order doesn't matter, but array order does matter. +/// Now includes special handling for DateTime string formats with improved null handling. +/// +public class JsonElementComparer : IEqualityComparer +{ + private string _failurePath = string.Empty; + + /// + public bool Equals(JsonElement x, JsonElement y) + { + _failurePath = string.Empty; + return CompareJsonElements(x, y, string.Empty); + } + + /// + public int GetHashCode(JsonElement obj) + { + return JsonSerializer.Serialize(obj).GetHashCode(); + } + + private bool CompareJsonElements(JsonElement x, JsonElement y, string path) + { + // If value kinds don't match, they're not equivalent + if (x.ValueKind != y.ValueKind) + { + _failurePath = $"{path}: Expected {x.ValueKind} but got {y.ValueKind}"; + return false; + } + + switch (x.ValueKind) + { + case JsonValueKind.Object: + return CompareJsonObjects(x, y, path); + + case JsonValueKind.Array: + return CompareJsonArraysInOrder(x, y, path); + + case JsonValueKind.String: + string? xStr = x.GetString(); + string? yStr = y.GetString(); + + // Handle null strings + if (xStr is null && yStr is null) + return true; + + if (xStr is null || yStr is null) + { + _failurePath = + $"{path}: Expected {(xStr is null ? "null" : $"\"{xStr}\"")} but got {(yStr is null ? "null" : $"\"{yStr}\"")}"; + return false; + } + + // Check if they are identical strings + if (xStr == yStr) + return true; + + // Try to handle DateTime strings + if (IsLikelyDateTimeString(xStr) && IsLikelyDateTimeString(yStr)) + { + if (AreEquivalentDateTimeStrings(xStr, yStr)) + return true; + } + + _failurePath = $"{path}: Expected \"{xStr}\" but got \"{yStr}\""; + return false; + + case JsonValueKind.Number: + if (x.GetDecimal() != y.GetDecimal()) + { + _failurePath = $"{path}: Expected {x.GetDecimal()} but got {y.GetDecimal()}"; + return false; + } + + return true; + + case JsonValueKind.True: + case JsonValueKind.False: + if (x.GetBoolean() != y.GetBoolean()) + { + _failurePath = $"{path}: Expected {x.GetBoolean()} but got {y.GetBoolean()}"; + return false; + } + + return true; + + case JsonValueKind.Null: + return true; + + default: + _failurePath = $"{path}: Unsupported JsonValueKind {x.ValueKind}"; + return false; + } + } + + private bool IsLikelyDateTimeString(string? str) + { + // Simple heuristic to identify likely ISO date time strings + return str is not null + && (str.Contains("T") && (str.EndsWith("Z") || str.Contains("+") || str.Contains("-"))); + } + + private bool AreEquivalentDateTimeStrings(string str1, string str2) + { + // Try to parse both as DateTime + if (DateTime.TryParse(str1, out DateTime dt1) && DateTime.TryParse(str2, out DateTime dt2)) + { + return dt1 == dt2; + } + + return false; + } + + private bool CompareJsonObjects(JsonElement x, JsonElement y, string path) + { + // Create dictionaries for both JSON objects + var xProps = new Dictionary(); + var yProps = new Dictionary(); + + foreach (var prop in x.EnumerateObject()) + xProps[prop.Name] = prop.Value; + + foreach (var prop in y.EnumerateObject()) + yProps[prop.Name] = prop.Value; + + // Check if all properties in x exist in y + foreach (var key in xProps.Keys) + { + if (!yProps.ContainsKey(key)) + { + _failurePath = $"{path}: Missing property '{key}'"; + return false; + } + } + + // Check if y has extra properties + foreach (var key in yProps.Keys) + { + if (!xProps.ContainsKey(key)) + { + _failurePath = $"{path}: Unexpected property '{key}'"; + return false; + } + } + + // Compare each property value + foreach (var key in xProps.Keys) + { + var propPath = string.IsNullOrEmpty(path) ? key : $"{path}.{key}"; + if (!CompareJsonElements(xProps[key], yProps[key], propPath)) + { + return false; + } + } + + return true; + } + + private bool CompareJsonArraysInOrder(JsonElement x, JsonElement y, string path) + { + var xArray = x.EnumerateArray(); + var yArray = y.EnumerateArray(); + + // Count x elements + var xCount = 0; + var xElements = new List(); + foreach (var item in xArray) + { + xElements.Add(item); + xCount++; + } + + // Count y elements + var yCount = 0; + var yElements = new List(); + foreach (var item in yArray) + { + yElements.Add(item); + yCount++; + } + + // Check if counts match + if (xCount != yCount) + { + _failurePath = $"{path}: Expected {xCount} items but found {yCount}"; + return false; + } + + // Compare elements in order + for (var i = 0; i < xCount; i++) + { + var itemPath = $"{path}[{i}]"; + if (!CompareJsonElements(xElements[i], yElements[i], itemPath)) + { + return false; + } + } + + return true; + } + + /// + public override string ToString() + { + if (!string.IsNullOrEmpty(_failurePath)) + { + return $"JSON comparison failed at {_failurePath}"; + } + + return "JsonElementEqualityComparer"; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/NUnitExtensions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/NUnitExtensions.cs new file mode 100644 index 000000000000..170795cb2c3e --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/NUnitExtensions.cs @@ -0,0 +1,30 @@ +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for NUnit constraints. +/// +public static class NUnitExtensions +{ + /// + /// Modifies the EqualConstraint to use our own set of default comparers. + /// + /// + /// + public static EqualConstraint UsingDefaults(this EqualConstraint constraint) => + constraint + .UsingPropertiesComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingReadOnlyMemoryComparer() + .UsingOneOfComparer() + .UsingJsonElementComparer() + .UsingOptionalComparer() + .UsingAdditionalPropertiesComparer(); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OneOfComparer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OneOfComparer.cs new file mode 100644 index 000000000000..767439174363 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OneOfComparer.cs @@ -0,0 +1,86 @@ +using NUnit.Framework.Constraints; +using OneOf; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle OneOf values. +/// +public static class EqualConstraintExtensions +{ + /// + /// Modifies the EqualConstraint to handle OneOf instances by comparing their inner values. + /// This works alongside other comparison modifiers like UsingPropertiesComparer. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingOneOfComparer(this EqualConstraint constraint) + { + // Register a comparer factory for IOneOf types + constraint.Using( + (x, y) => + { + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (x.Value is null && y.Value is null) + { + return true; + } + + if (x.Value is null) + { + return false; + } + + var propertiesComparer = new NUnitEqualityComparer(); + var tolerance = Tolerance.Default; + propertiesComparer.CompareProperties = true; + // Add OneOf comparer to handle nested OneOf values (e.g., in Lists) + propertiesComparer.ExternalComparers.Add( + new OneOfEqualityAdapter(propertiesComparer) + ); + return propertiesComparer.AreEqual(x.Value, y.Value, ref tolerance); + } + ); + + return constraint; + } + + /// + /// EqualityAdapter for comparing IOneOf instances within NUnitEqualityComparer. + /// This enables recursive comparison of nested OneOf values. + /// + private class OneOfEqualityAdapter : EqualityAdapter + { + private readonly NUnitEqualityComparer _comparer; + + public OneOfEqualityAdapter(NUnitEqualityComparer comparer) + { + _comparer = comparer; + } + + public override bool CanCompare(object? x, object? y) + { + return x is IOneOf && y is IOneOf; + } + + public override bool AreEqual(object? x, object? y) + { + var oneOfX = (IOneOf?)x; + var oneOfY = (IOneOf?)y; + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (oneOfX?.Value is null && oneOfY?.Value is null) + { + return true; + } + + if (oneOfX?.Value is null || oneOfY?.Value is null) + { + return false; + } + + var tolerance = Tolerance.Default; + return _comparer.AreEqual(oneOfX.Value, oneOfY.Value, ref tolerance); + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OptionalComparer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OptionalComparer.cs new file mode 100644 index 000000000000..139a54732317 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/OptionalComparer.cs @@ -0,0 +1,104 @@ +using NUnit.Framework.Constraints; +using OneOf; +using SeedSimpleApi.Core; + +namespace NUnit.Framework; + +/// +/// Extensions for EqualConstraint to handle Optional values. +/// +public static class OptionalComparerExtensions +{ + /// + /// Modifies the EqualConstraint to handle Optional instances by comparing their IsDefined state and inner values. + /// This works alongside other comparison modifiers like UsingPropertiesComparer. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingOptionalComparer(this EqualConstraint constraint) + { + // Register a comparer factory for IOptional types + constraint.Using( + (x, y) => + { + // Both must have the same IsDefined state + if (x.IsDefined != y.IsDefined) + { + return false; + } + + // If both are undefined, they're equal + if (!x.IsDefined) + { + return true; + } + + // Both are defined, compare their boxed values + var xValue = x.GetBoxedValue(); + var yValue = y.GetBoxedValue(); + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (xValue is null && yValue is null) + { + return true; + } + + if (xValue is null || yValue is null) + { + return false; + } + + // Use NUnit's property comparer for the inner values + var propertiesComparer = new NUnitEqualityComparer(); + var tolerance = Tolerance.Default; + propertiesComparer.CompareProperties = true; + // Add OneOf comparer to handle nested OneOf values (e.g., in Lists within Optional) + propertiesComparer.ExternalComparers.Add( + new OneOfEqualityAdapter(propertiesComparer) + ); + return propertiesComparer.AreEqual(xValue, yValue, ref tolerance); + } + ); + + return constraint; + } + + /// + /// EqualityAdapter for comparing IOneOf instances within NUnitEqualityComparer. + /// This enables recursive comparison of nested OneOf values within Optional types. + /// + private class OneOfEqualityAdapter : EqualityAdapter + { + private readonly NUnitEqualityComparer _comparer; + + public OneOfEqualityAdapter(NUnitEqualityComparer comparer) + { + _comparer = comparer; + } + + public override bool CanCompare(object? x, object? y) + { + return x is IOneOf && y is IOneOf; + } + + public override bool AreEqual(object? x, object? y) + { + var oneOfX = (IOneOf?)x; + var oneOfY = (IOneOf?)y; + + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (oneOfX?.Value is null && oneOfY?.Value is null) + { + return true; + } + + if (oneOfX?.Value is null || oneOfY?.Value is null) + { + return false; + } + + var tolerance = Tolerance.Default; + return _comparer.AreEqual(oneOfX.Value, oneOfY.Value, ref tolerance); + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/ReadOnlyMemoryComparer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/ReadOnlyMemoryComparer.cs new file mode 100644 index 000000000000..fc0b595a5e54 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Utils/ReadOnlyMemoryComparer.cs @@ -0,0 +1,87 @@ +using NUnit.Framework.Constraints; + +namespace NUnit.Framework; + +/// +/// Extensions for NUnit constraints. +/// +public static class ReadOnlyMemoryComparerExtensions +{ + /// + /// Extension method for comparing ReadOnlyMemory<T> in NUnit tests. + /// + /// The type of elements in the ReadOnlyMemory. + /// The Is.EqualTo() constraint instance. + /// A constraint that can compare ReadOnlyMemory<T>. + public static EqualConstraint UsingReadOnlyMemoryComparer(this EqualConstraint constraint) + where T : IComparable + { + return constraint.Using(new ReadOnlyMemoryComparer()); + } +} + +/// +/// Comparer for ReadOnlyMemory<T>. Compares sequences by value. +/// +/// +/// The type of elements in the ReadOnlyMemory. +/// +public class ReadOnlyMemoryComparer : IComparer> + where T : IComparable +{ + /// + public int Compare(ReadOnlyMemory x, ReadOnlyMemory y) + { + // Check if sequences are equal + var xSpan = x.Span; + var ySpan = y.Span; + + // Optimized case for IEquatable implementations + if (typeof(IEquatable).IsAssignableFrom(typeof(T))) + { + var areEqual = xSpan.SequenceEqual(ySpan); + if (areEqual) + { + return 0; // Sequences are equal + } + } + else + { + // Manual equality check for non-IEquatable types + if (xSpan.Length == ySpan.Length) + { + var areEqual = true; + for (var i = 0; i < xSpan.Length; i++) + { + if (!EqualityComparer.Default.Equals(xSpan[i], ySpan[i])) + { + areEqual = false; + break; + } + } + + if (areEqual) + { + return 0; // Sequences are equal + } + } + } + + // For non-equal sequences, we need to return a consistent ordering + // First compare lengths + if (x.Length != y.Length) + return x.Length.CompareTo(y.Length); + + // Same length but different content - compare first differing element + for (var i = 0; i < x.Length; i++) + { + if (!EqualityComparer.Default.Equals(xSpan[i], ySpan[i])) + { + return xSpan[i].CompareTo(ySpan[i]); + } + } + + // Should never reach here if not equal + return 0; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ApiResponse.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ApiResponse.cs new file mode 100644 index 000000000000..090ea5f105c6 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ApiResponse.cs @@ -0,0 +1,13 @@ +using global::System.Net.Http; + +namespace SeedSimpleApi.Core; + +/// +/// The response object returned from the API. +/// +internal record ApiResponse +{ + internal required int StatusCode { get; init; } + + internal required HttpResponseMessage Raw { get; init; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/BaseRequest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/BaseRequest.cs new file mode 100644 index 000000000000..c1f2c6b60ba1 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/BaseRequest.cs @@ -0,0 +1,67 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; +using global::System.Text; + +namespace SeedSimpleApi.Core; + +internal abstract record BaseRequest +{ + internal string? BaseUrl { get; init; } + + internal required HttpMethod Method { get; init; } + + internal required string Path { get; init; } + + internal string? ContentType { get; init; } + + /// + /// The query string for this request (including the leading '?' if non-empty). + /// + internal string? QueryString { get; init; } + + internal Dictionary Headers { get; init; } = + new(StringComparer.OrdinalIgnoreCase); + + internal IRequestOptions? Options { get; init; } + + internal abstract HttpContent? CreateContent(); + + protected static ( + Encoding encoding, + string? charset, + string mediaType + ) ParseContentTypeOrDefault( + string? contentType, + Encoding encodingFallback, + string mediaTypeFallback + ) + { + var encoding = encodingFallback; + var mediaType = mediaTypeFallback; + string? charset = null; + if (string.IsNullOrEmpty(contentType)) + { + return (encoding, charset, mediaType); + } + + if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaTypeHeaderValue)) + { + return (encoding, charset, mediaType); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + { + charset = mediaTypeHeaderValue.CharSet; + encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.MediaType)) + { + mediaType = mediaTypeHeaderValue.MediaType; + } + + return (encoding, charset, mediaType); + } + + protected static Encoding Utf8NoBom => EncodingCache.Utf8NoBom; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/CollectionItemSerializer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/CollectionItemSerializer.cs new file mode 100644 index 000000000000..38199139edac --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/CollectionItemSerializer.cs @@ -0,0 +1,89 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedSimpleApi.Core; + +/// +/// Json collection converter. +/// +/// Type of item to convert. +/// Converter to use for individual items. +internal class CollectionItemSerializer + : JsonConverter> + where TConverterType : JsonConverter +{ + /// + /// Reads a json string and deserializes it into an object. + /// + /// Json reader. + /// Type to convert. + /// Serializer options. + /// Created object. + public override IEnumerable? Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + + var returnValue = new List(); + + while (reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + var item = (TDatatype)( + JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions) + ?? throw new global::System.Exception( + $"Failed to deserialize collection item of type {typeof(TDatatype)}" + ) + ); + returnValue.Add(item); + } + + reader.Read(); + } + + return returnValue; + } + + /// + /// Writes a json string. + /// + /// Json writer. + /// Value to write. + /// Serializer options. + public override void Write( + Utf8JsonWriter writer, + IEnumerable? value, + JsonSerializerOptions options + ) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + var jsonSerializerOptions = new JsonSerializerOptions(options); + jsonSerializerOptions.Converters.Clear(); + jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + + writer.WriteStartArray(); + + foreach (var data in value) + { + JsonSerializer.Serialize(writer, data, jsonSerializerOptions); + } + + writer.WriteEndArray(); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Constants.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Constants.cs new file mode 100644 index 000000000000..ca0bb4c1324f --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Constants.cs @@ -0,0 +1,7 @@ +namespace SeedSimpleApi.Core; + +internal static class Constants +{ + public const string DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK"; + public const string DateFormat = "yyyy-MM-dd"; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateOnlyConverter.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateOnlyConverter.cs new file mode 100644 index 000000000000..5f6c106a26e4 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateOnlyConverter.cs @@ -0,0 +1,747 @@ +// ReSharper disable All +#pragma warning disable + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using global::System.Diagnostics; +using global::System.Diagnostics.CodeAnalysis; +using global::System.Globalization; +using global::System.Runtime.CompilerServices; +using global::System.Runtime.InteropServices; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable SuggestVarOrType_BuiltInTypes + +namespace SeedSimpleApi.Core +{ + /// + /// Custom converter for handling the data type with the System.Text.Json library. + /// + /// + /// This class backported from: + /// + /// System.Text.Json.Serialization.Converters.DateOnlyConverter + /// + public sealed class DateOnlyConverter : JsonConverter + { + private const int FormatLength = 10; // YYYY-MM-DD + + private const int MaxEscapedFormatLength = + FormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; + + /// + public override DateOnly Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType); + } + + return ReadCore(ref reader); + } + + /// + public override DateOnly ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + return ReadCore(ref reader); + } + + private static DateOnly ReadCore(ref Utf8JsonReader reader) + { + if ( + !JsonHelpers.IsInRangeInclusive( + reader.ValueLength(), + FormatLength, + MaxEscapedFormatLength + ) + ) + { + ThrowHelper.ThrowFormatException(DataType.DateOnly); + } + + scoped ReadOnlySpan source; + if (!reader.HasValueSequence && !reader.ValueIsEscaped) + { + source = reader.ValueSpan; + } + else + { + Span stackSpan = stackalloc byte[MaxEscapedFormatLength]; + int bytesWritten = reader.CopyString(stackSpan); + source = stackSpan.Slice(0, bytesWritten); + } + + if (!JsonHelpers.TryParseAsIso(source, out DateOnly value)) + { + ThrowHelper.ThrowFormatException(DataType.DateOnly); + } + + return value; + } + + /// + public override void Write( + Utf8JsonWriter writer, + DateOnly value, + JsonSerializerOptions options + ) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[FormatLength]; +#else + Span buffer = stackalloc char[FormatLength]; +#endif + // ReSharper disable once RedundantAssignment + bool formattedSuccessfully = value.TryFormat( + buffer, + out int charsWritten, + "O".AsSpan(), + CultureInfo.InvariantCulture + ); + Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); + writer.WriteStringValue(buffer); + } + + /// + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + DateOnly value, + JsonSerializerOptions options + ) + { +#if NET8_0_OR_GREATER + Span buffer = stackalloc byte[FormatLength]; +#else + Span buffer = stackalloc char[FormatLength]; +#endif + // ReSharper disable once RedundantAssignment + bool formattedSuccessfully = value.TryFormat( + buffer, + out int charsWritten, + "O".AsSpan(), + CultureInfo.InvariantCulture + ); + Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); + writer.WritePropertyName(buffer); + } + } + + internal static class JsonConstants + { + // The maximum number of fraction digits the Json DateTime parser allows + public const int DateTimeParseNumFractionDigits = 16; + + // In the worst case, an ASCII character represented as a single utf-8 byte could expand 6x when escaped. + public const int MaxExpansionFactorWhileEscaping = 6; + + // The largest fraction expressible by TimeSpan and DateTime formats + public const int MaxDateTimeFraction = 9_999_999; + + // TimeSpan and DateTime formats allow exactly up to many digits for specifying the fraction after the seconds. + public const int DateTimeNumFractionDigits = 7; + + public const byte UtcOffsetToken = (byte)'Z'; + + public const byte TimePrefix = (byte)'T'; + + public const byte Period = (byte)'.'; + + public const byte Hyphen = (byte)'-'; + + public const byte Colon = (byte)':'; + + public const byte Plus = (byte)'+'; + } + + // ReSharper disable SuggestVarOrType_Elsewhere + // ReSharper disable SuggestVarOrType_SimpleTypes + // ReSharper disable SuggestVarOrType_BuiltInTypes + + internal static class JsonHelpers + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRangeInclusive(int value, int lowerBound, int upperBound) => + (uint)(value - lowerBound) <= (uint)(upperBound - lowerBound); + + public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0'; + + [StructLayout(LayoutKind.Auto)] + private struct DateTimeParseData + { + public int Year; + public int Month; + public int Day; + public bool IsCalendarDateOnly; + public int Hour; + public int Minute; + public int Second; + public int Fraction; // This value should never be greater than 9_999_999. + public int OffsetHours; + public int OffsetMinutes; + + // ReSharper disable once NotAccessedField.Local + public byte OffsetToken; + } + + public static bool TryParseAsIso(ReadOnlySpan source, out DateOnly value) + { + if ( + TryParseDateTimeOffset(source, out DateTimeParseData parseData) + && parseData.IsCalendarDateOnly + && TryCreateDateTime(parseData, DateTimeKind.Unspecified, out DateTime dateTime) + ) + { + value = DateOnly.FromDateTime(dateTime); + return true; + } + + value = default; + return false; + } + + /// + /// ISO 8601 date time parser (ISO 8601-1:2019). + /// + /// The date/time to parse in UTF-8 format. + /// The parsed for the given . + /// + /// Supports extended calendar date (5.2.2.1) and complete (5.4.2.1) calendar date/time of day + /// representations with optional specification of seconds and fractional seconds. + /// + /// Times can be explicitly specified as UTC ("Z" - 5.3.3) or offsets from UTC ("+/-hh:mm" 5.3.4.2). + /// If unspecified they are considered to be local per spec. + /// + /// Examples: (TZD is either "Z" or hh:mm offset from UTC) + /// + /// YYYY-MM-DD (e.g. 1997-07-16) + /// YYYY-MM-DDThh:mm (e.g. 1997-07-16T19:20) + /// YYYY-MM-DDThh:mm:ss (e.g. 1997-07-16T19:20:30) + /// YYYY-MM-DDThh:mm:ss.s (e.g. 1997-07-16T19:20:30.45) + /// YYYY-MM-DDThh:mmTZD (e.g. 1997-07-16T19:20+01:00) + /// YYYY-MM-DDThh:mm:ssTZD (e.g. 1997-07-16T19:20:3001:00) + /// YYYY-MM-DDThh:mm:ss.sTZD (e.g. 1997-07-16T19:20:30.45Z) + /// + /// Generally speaking we always require the "extended" option when one exists (3.1.3.5). + /// The extended variants have separator characters between components ('-', ':', '.', etc.). + /// Spaces are not permitted. + /// + /// "true" if successfully parsed. + private static bool TryParseDateTimeOffset( + ReadOnlySpan source, + out DateTimeParseData parseData + ) + { + parseData = default; + + // too short datetime + Debug.Assert(source.Length >= 10); + + // Parse the calendar date + // ----------------------- + // ISO 8601-1:2019 5.2.2.1b "Calendar date complete extended format" + // [dateX] = [year]["-"][month]["-"][day] + // [year] = [YYYY] [0000 - 9999] (4.3.2) + // [month] = [MM] [01 - 12] (4.3.3) + // [day] = [DD] [01 - 28, 29, 30, 31] (4.3.4) + // + // Note: 5.2.2.2 "Representations with reduced precision" allows for + // just [year]["-"][month] (a) and just [year] (b), but we currently + // don't permit it. + + { + uint digit1 = source[0] - (uint)'0'; + uint digit2 = source[1] - (uint)'0'; + uint digit3 = source[2] - (uint)'0'; + uint digit4 = source[3] - (uint)'0'; + + if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9) + { + return false; + } + + parseData.Year = (int)(digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4); + } + + if ( + source[4] != JsonConstants.Hyphen + || !TryGetNextTwoDigits(source.Slice(start: 5, length: 2), ref parseData.Month) + || source[7] != JsonConstants.Hyphen + || !TryGetNextTwoDigits(source.Slice(start: 8, length: 2), ref parseData.Day) + ) + { + return false; + } + + // We now have YYYY-MM-DD [dateX] + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (source.Length == 10) + { + parseData.IsCalendarDateOnly = true; + return true; + } + + // Parse the time of day + // --------------------- + // + // ISO 8601-1:2019 5.3.1.2b "Local time of day complete extended format" + // [timeX] = ["T"][hour][":"][min][":"][sec] + // [hour] = [hh] [00 - 23] (4.3.8a) + // [minute] = [mm] [00 - 59] (4.3.9a) + // [sec] = [ss] [00 - 59, 60 with a leap second] (4.3.10a) + // + // ISO 8601-1:2019 5.3.3 "UTC of day" + // [timeX]["Z"] + // + // ISO 8601-1:2019 5.3.4.2 "Local time of day with the time shift between + // local timescale and UTC" (Extended format) + // + // [shiftX] = ["+"|"-"][hour][":"][min] + // + // Notes: + // + // "T" is optional per spec, but _only_ when times are used alone. In our + // case, we're reading out a complete date & time and as such require "T". + // (5.4.2.1b). + // + // For [timeX] We allow seconds to be omitted per 5.3.1.3a "Representations + // with reduced precision". 5.3.1.3b allows just specifying the hour, but + // we currently don't permit this. + // + // Decimal fractions are allowed for hours, minutes and seconds (5.3.14). + // We only allow fractions for seconds currently. Lower order components + // can't follow, i.e. you can have T23.3, but not T23.3:04. There must be + // one digit, but the max number of digits is implementation defined. We + // currently allow up to 16 digits of fractional seconds only. While we + // support 16 fractional digits we only parse the first seven, anything + // past that is considered a zero. This is to stay compatible with the + // DateTime implementation which is limited to this resolution. + + if (source.Length < 16) + { + // Source does not have enough characters for YYYY-MM-DDThh:mm + return false; + } + + // Parse THH:MM (e.g. "T10:32") + if ( + source[10] != JsonConstants.TimePrefix + || source[13] != JsonConstants.Colon + || !TryGetNextTwoDigits(source.Slice(start: 11, length: 2), ref parseData.Hour) + || !TryGetNextTwoDigits(source.Slice(start: 14, length: 2), ref parseData.Minute) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm + Debug.Assert(source.Length >= 16); + if (source.Length == 16) + { + return true; + } + + byte curByte = source[16]; + int sourceIndex = 17; + + // Either a TZD ['Z'|'+'|'-'] or a seconds separator [':'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + case JsonConstants.Colon: + break; + default: + return false; + } + + // Try reading the seconds + if ( + source.Length < 19 + || !TryGetNextTwoDigits(source.Slice(start: 17, length: 2), ref parseData.Second) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm:ss + Debug.Assert(source.Length >= 19); + if (source.Length == 19) + { + return true; + } + + curByte = source[19]; + sourceIndex = 20; + + // Either a TZD ['Z'|'+'|'-'] or a seconds decimal fraction separator ['.'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + case JsonConstants.Period: + break; + default: + return false; + } + + // Source does not have enough characters for second fractions (i.e. ".s") + // YYYY-MM-DDThh:mm:ss.s + if (source.Length < 21) + { + return false; + } + + // Parse fraction. This value should never be greater than 9_999_999 + int numDigitsRead = 0; + int fractionEnd = Math.Min( + sourceIndex + JsonConstants.DateTimeParseNumFractionDigits, + source.Length + ); + + while (sourceIndex < fractionEnd && IsDigit(curByte = source[sourceIndex])) + { + if (numDigitsRead < JsonConstants.DateTimeNumFractionDigits) + { + parseData.Fraction = parseData.Fraction * 10 + (int)(curByte - (uint)'0'); + numDigitsRead++; + } + + sourceIndex++; + } + + if (parseData.Fraction != 0) + { + while (numDigitsRead < JsonConstants.DateTimeNumFractionDigits) + { + parseData.Fraction *= 10; + numDigitsRead++; + } + } + + // We now have YYYY-MM-DDThh:mm:ss.s + Debug.Assert(sourceIndex <= source.Length); + if (sourceIndex == source.Length) + { + return true; + } + + curByte = source[sourceIndex++]; + + // TZD ['Z'|'+'|'-'] is valid at this point + switch (curByte) + { + case JsonConstants.UtcOffsetToken: + parseData.OffsetToken = JsonConstants.UtcOffsetToken; + return sourceIndex == source.Length; + case JsonConstants.Plus: + case JsonConstants.Hyphen: + parseData.OffsetToken = curByte; + return ParseOffset(ref parseData, source.Slice(sourceIndex)); + default: + return false; + } + + static bool ParseOffset(ref DateTimeParseData parseData, ReadOnlySpan offsetData) + { + // Parse the hours for the offset + if ( + offsetData.Length < 2 + || !TryGetNextTwoDigits(offsetData.Slice(0, 2), ref parseData.OffsetHours) + ) + { + return false; + } + + // We now have YYYY-MM-DDThh:mm:ss.s+|-hh + + if (offsetData.Length == 2) + { + // Just hours offset specified + return true; + } + + // Ensure we have enough for ":mm" + return offsetData.Length == 5 + && offsetData[2] == JsonConstants.Colon + && TryGetNextTwoDigits(offsetData.Slice(3), ref parseData.OffsetMinutes); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + // ReSharper disable once RedundantAssignment + private static bool TryGetNextTwoDigits(ReadOnlySpan source, ref int value) + { + Debug.Assert(source.Length == 2); + + uint digit1 = source[0] - (uint)'0'; + uint digit2 = source[1] - (uint)'0'; + + if (digit1 > 9 || digit2 > 9) + { + value = 0; + return false; + } + + value = (int)(digit1 * 10 + digit2); + return true; + } + + // The following methods are borrowed verbatim from src/Common/src/CoreLib/System/Buffers/Text/Utf8Parser/Utf8Parser.Date.Helpers.cs + + /// + /// Overflow-safe DateTime factory. + /// + private static bool TryCreateDateTime( + DateTimeParseData parseData, + DateTimeKind kind, + out DateTime value + ) + { + if (parseData.Year == 0) + { + value = default; + return false; + } + + Debug.Assert(parseData.Year <= 9999); // All of our callers to date parse the year from fixed 4-digit fields so this value is trusted. + + if ((uint)parseData.Month - 1 >= 12) + { + value = default; + return false; + } + + uint dayMinusOne = (uint)parseData.Day - 1; + if ( + dayMinusOne >= 28 + && dayMinusOne >= DateTime.DaysInMonth(parseData.Year, parseData.Month) + ) + { + value = default; + return false; + } + + if ((uint)parseData.Hour > 23) + { + value = default; + return false; + } + + if ((uint)parseData.Minute > 59) + { + value = default; + return false; + } + + // This needs to allow leap seconds when appropriate. + // See https://github.com/dotnet/runtime/issues/30135. + if ((uint)parseData.Second > 59) + { + value = default; + return false; + } + + Debug.Assert(parseData.Fraction is >= 0 and <= JsonConstants.MaxDateTimeFraction); // All of our callers to date parse the fraction from fixed 7-digit fields so this value is trusted. + + ReadOnlySpan days = DateTime.IsLeapYear(parseData.Year) + ? DaysToMonth366 + : DaysToMonth365; + int yearMinusOne = parseData.Year - 1; + int totalDays = + yearMinusOne * 365 + + yearMinusOne / 4 + - yearMinusOne / 100 + + yearMinusOne / 400 + + days[parseData.Month - 1] + + parseData.Day + - 1; + long ticks = totalDays * TimeSpan.TicksPerDay; + int totalSeconds = parseData.Hour * 3600 + parseData.Minute * 60 + parseData.Second; + ticks += totalSeconds * TimeSpan.TicksPerSecond; + ticks += parseData.Fraction; + value = new DateTime(ticks: ticks, kind: kind); + return true; + } + + private static ReadOnlySpan DaysToMonth365 => + [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]; + private static ReadOnlySpan DaysToMonth366 => + [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]; + } + + internal static class ThrowHelper + { + private const string ExceptionSourceValueToRethrowAsJsonException = + "System.Text.Json.Rethrowable"; + + [DoesNotReturn] + public static void ThrowInvalidOperationException_ExpectedString(JsonTokenType tokenType) + { + throw GetInvalidOperationException("string", tokenType); + } + + public static void ThrowFormatException(DataType dataType) + { + throw new FormatException(SR.Format(SR.UnsupportedFormat, dataType)) + { + Source = ExceptionSourceValueToRethrowAsJsonException, + }; + } + + private static global::System.Exception GetInvalidOperationException( + string message, + JsonTokenType tokenType + ) + { + return GetInvalidOperationException(SR.Format(SR.InvalidCast, tokenType, message)); + } + + private static InvalidOperationException GetInvalidOperationException(string message) + { + return new InvalidOperationException(message) + { + Source = ExceptionSourceValueToRethrowAsJsonException, + }; + } + } + + internal static class Utf8JsonReaderExtensions + { + internal static int ValueLength(this Utf8JsonReader reader) => + reader.HasValueSequence + ? checked((int)reader.ValueSequence.Length) + : reader.ValueSpan.Length; + } + + internal enum DataType + { + TimeOnly, + DateOnly, + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class SR + { + private static readonly bool s_usingResourceKeys = + AppContext.TryGetSwitch( + "System.Resources.UseSystemResourceKeys", + out bool usingResourceKeys + ) && usingResourceKeys; + + public static string UnsupportedFormat => Strings.UnsupportedFormat; + + public static string InvalidCast => Strings.InvalidCast; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string Format(string resourceFormat, object? p1) => + s_usingResourceKeys + ? string.Join(", ", resourceFormat, p1) + : string.Format(resourceFormat, p1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string Format(string resourceFormat, object? p1, object? p2) => + s_usingResourceKeys + ? string.Join(", ", resourceFormat, p1, p2) + : string.Format(resourceFormat, p1, p2); + } + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute( + "System.Resources.Tools.StronglyTypedResourceBuilder", + "17.0.0.0" + )] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings + { + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "Microsoft.Performance", + "CA1811:AvoidUncalledPrivateCode" + )] + internal Strings() { } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute( + global::System.ComponentModel.EditorBrowsableState.Advanced + )] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = + new global::System.Resources.ResourceManager( + "System.Text.Json.Resources.Strings", + typeof(Strings).Assembly + ); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute( + global::System.ComponentModel.EditorBrowsableState.Advanced + )] + internal static global::System.Globalization.CultureInfo Culture + { + get { return resourceCulture; } + set { resourceCulture = value; } + } + + /// + /// Looks up a localized string similar to Cannot get the value of a token type '{0}' as a {1}.. + /// + internal static string InvalidCast + { + get { return ResourceManager.GetString("InvalidCast", resourceCulture); } + } + + /// + /// Looks up a localized string similar to The JSON value is not in a supported {0} format.. + /// + internal static string UnsupportedFormat + { + get { return ResourceManager.GetString("UnsupportedFormat", resourceCulture); } + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateTimeSerializer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateTimeSerializer.cs new file mode 100644 index 000000000000..ed799772a455 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/DateTimeSerializer.cs @@ -0,0 +1,22 @@ +using global::System.Globalization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedSimpleApi.Core; + +internal class DateTimeSerializer : JsonConverter +{ + public override DateTime Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(Constants.DateTimeFormat)); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EmptyRequest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EmptyRequest.cs new file mode 100644 index 000000000000..7aebf82b2884 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EmptyRequest.cs @@ -0,0 +1,11 @@ +using global::System.Net.Http; + +namespace SeedSimpleApi.Core; + +/// +/// The request object to send without a request body. +/// +internal record EmptyRequest : BaseRequest +{ + internal override HttpContent? CreateContent() => null; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EncodingCache.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EncodingCache.cs new file mode 100644 index 000000000000..5eac2837373b --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/EncodingCache.cs @@ -0,0 +1,11 @@ +using global::System.Text; + +namespace SeedSimpleApi.Core; + +internal static class EncodingCache +{ + internal static readonly Encoding Utf8NoBom = new UTF8Encoding( + encoderShouldEmitUTF8Identifier: false, + throwOnInvalidBytes: true + ); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Extensions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Extensions.cs new file mode 100644 index 000000000000..a0dd0f7595ef --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Extensions.cs @@ -0,0 +1,55 @@ +using global::System.Diagnostics.CodeAnalysis; +using global::System.Runtime.Serialization; + +namespace SeedSimpleApi.Core; + +internal static class Extensions +{ + public static string Stringify(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + if (field is not null) + { + var attribute = (EnumMemberAttribute?) + global::System.Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)); + return attribute?.Value ?? value.ToString(); + } + return value.ToString(); + } + + /// + /// Asserts that a condition is true, throwing an exception with the specified message if it is false. + /// + /// The condition to assert. + /// The exception message if the assertion fails. + /// Thrown when the condition is false. + internal static void Assert(this object value, bool condition, string message) + { + if (!condition) + { + throw new global::System.Exception(message); + } + } + + /// + /// Asserts that a value is not null, throwing an exception with the specified message if it is null. + /// + /// The type of the value to assert. + /// The value to assert is not null. + /// The exception message if the assertion fails. + /// The non-null value. + /// Thrown when the value is null. + internal static TValue Assert( + this object _unused, + [NotNull] TValue? value, + string message + ) + where TValue : class + { + if (value is null) + { + throw new global::System.Exception(message); + } + return value; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/FormUrlEncoder.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/FormUrlEncoder.cs new file mode 100644 index 000000000000..b6a4c6f832fd --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/FormUrlEncoder.cs @@ -0,0 +1,33 @@ +using global::System.Net.Http; + +namespace SeedSimpleApi.Core; + +/// +/// Encodes an object into a form URL-encoded content. +/// +public static class FormUrlEncoder +{ + /// + /// Encodes an object into a form URL-encoded content using Deep Object notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsDeepObject(object value) => + new(QueryStringConverter.ToDeepObject(value)); + + /// + /// Encodes an object into a form URL-encoded content using Exploded Form notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsExplodedForm(object value) => + new(QueryStringConverter.ToExplodedForm(value)); + + /// + /// Encodes an object into a form URL-encoded content using Form notation without exploding parameters. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + internal static FormUrlEncodedContent EncodeAsForm(object value) => + new(QueryStringConverter.ToForm(value)); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeaderValue.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeaderValue.cs new file mode 100644 index 000000000000..32aa749d55cf --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeaderValue.cs @@ -0,0 +1,52 @@ +namespace SeedSimpleApi.Core; + +internal sealed class HeaderValue +{ + private readonly Func> _resolver; + + public HeaderValue(string value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value); + } + + public HeaderValue(Func value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } + + public HeaderValue(Func> value) + { + _resolver = value; + } + + public HeaderValue(Func> value) + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } + + public static implicit operator HeaderValue(string value) => new(value); + + public static implicit operator HeaderValue(Func value) => new(value); + + public static implicit operator HeaderValue( + Func> value + ) => new(value); + + public static implicit operator HeaderValue( + Func> value + ) => new(value); + + public static HeaderValue FromString(string value) => new(value); + + public static HeaderValue FromFunc(Func value) => new(value); + + public static HeaderValue FromValueTaskFunc( + Func> value + ) => new(value); + + public static HeaderValue FromTaskFunc( + Func> value + ) => new(value); + + internal global::System.Threading.Tasks.ValueTask ResolveAsync() => _resolver(); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Headers.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Headers.cs new file mode 100644 index 000000000000..448e569f0fbb --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Headers.cs @@ -0,0 +1,28 @@ +namespace SeedSimpleApi.Core; + +/// +/// Represents the headers sent with the request. +/// +internal sealed class Headers : Dictionary +{ + internal Headers() { } + + /// + /// Initializes a new instance of the Headers class with the specified value. + /// + /// + internal Headers(Dictionary value) + { + foreach (var kvp in value) + { + this[kvp.Key] = kvp.Value; + } + } + + /// + /// Initializes a new instance of the Headers class with the specified value. + /// + /// + internal Headers(IEnumerable> value) + : base(value.ToDictionary(e => e.Key, e => e.Value)) { } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeadersBuilder.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeadersBuilder.cs new file mode 100644 index 000000000000..9919ab74bcc8 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HeadersBuilder.cs @@ -0,0 +1,197 @@ +namespace SeedSimpleApi.Core; + +/// +/// Fluent builder for constructing HTTP headers with support for merging from multiple sources. +/// Provides a clean API for building headers with proper precedence handling. +/// +internal static class HeadersBuilder +{ + /// + /// Fluent builder for constructing HTTP headers. + /// + public sealed class Builder + { + private readonly Dictionary _headers; + + /// + /// Initializes a new instance with default capacity. + /// Uses case-insensitive header name comparison. + /// + public Builder() + { + _headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Initializes a new instance with the specified initial capacity. + /// Uses case-insensitive header name comparison. + /// + public Builder(int capacity) + { + _headers = new Dictionary( + capacity, + StringComparer.OrdinalIgnoreCase + ); + } + + /// + /// Adds a header with the specified key and value. + /// If a header with the same key already exists, it will be overwritten. + /// Null values are ignored. + /// + /// The header name. + /// The header value. Null values are ignored. + /// This builder instance for method chaining. + public Builder Add(string key, string? value) + { + if (value is not null) + { + _headers[key] = (value); + } + return this; + } + + /// + /// Adds a header with the specified key and object value. + /// The value will be converted to string using ValueConvert for consistent serialization. + /// If a header with the same key already exists, it will be overwritten. + /// Null values are ignored. + /// + /// The header name. + /// The header value. Null values are ignored. + /// This builder instance for method chaining. + public Builder Add(string key, object? value) + { + if (value is null) + { + return this; + } + + // Use ValueConvert for consistent serialization across headers, query params, and path params + var stringValue = ValueConvert.ToString(value); + if (stringValue is not null) + { + _headers[key] = (stringValue); + } + return this; + } + + /// + /// Adds multiple headers from a Headers dictionary. + /// HeaderValue instances are stored and will be resolved when BuildAsync() is called. + /// Overwrites any existing headers with the same key. + /// Null entries are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(Headers? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + _headers[header.Key] = header.Value; + } + + return this; + } + + /// + /// Adds multiple headers from a Headers dictionary, excluding the Authorization header. + /// This is useful for endpoints that don't require authentication, to avoid triggering + /// lazy auth token resolution. + /// HeaderValue instances are stored and will be resolved when BuildAsync() is called. + /// Overwrites any existing headers with the same key. + /// Null entries are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder AddWithoutAuth(Headers? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + _headers[header.Key] = header.Value; + } + + return this; + } + + /// + /// Adds multiple headers from a key-value pair collection. + /// Overwrites any existing headers with the same key. + /// Null values are ignored. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(IEnumerable>? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + if (header.Value is not null) + { + _headers[header.Key] = (header.Value); + } + } + + return this; + } + + /// + /// Adds multiple headers from a dictionary. + /// Overwrites any existing headers with the same key. + /// + /// The headers to add. Null is treated as empty. + /// This builder instance for method chaining. + public Builder Add(Dictionary? headers) + { + if (headers is null) + { + return this; + } + + foreach (var header in headers) + { + _headers[header.Key] = (header.Value); + } + + return this; + } + + /// + /// Asynchronously builds the final headers dictionary containing all merged headers. + /// Resolves all HeaderValue instances that may contain async operations. + /// Returns a case-insensitive dictionary. + /// + /// A task that represents the asynchronous operation, containing a case-insensitive dictionary of headers. + public async global::System.Threading.Tasks.Task> BuildAsync() + { + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _headers) + { + var value = await kvp.Value.ResolveAsync().ConfigureAwait(false); + if (value is not null) + { + headers[kvp.Key] = value; + } + } + return headers; + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpContentExtensions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpContentExtensions.cs new file mode 100644 index 000000000000..64ed69dce54d --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpContentExtensions.cs @@ -0,0 +1,20 @@ +#if !NET5_0_OR_GREATER +namespace SeedSimpleApi.Core; + +/// +/// Polyfill extension providing a ReadAsStringAsync(CancellationToken) overload +/// for target frameworks older than .NET 5, where only the parameterless +/// ReadAsStringAsync() is available. +/// +internal static class HttpContentExtensions +{ + internal static Task ReadAsStringAsync( + this HttpContent httpContent, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return httpContent.ReadAsStringAsync(); + } +} +#endif diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpMethodExtensions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpMethodExtensions.cs new file mode 100644 index 000000000000..529c0bd73e1c --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/HttpMethodExtensions.cs @@ -0,0 +1,8 @@ +using global::System.Net.Http; + +namespace SeedSimpleApi.Core; + +internal static class HttpMethodExtensions +{ + public static readonly HttpMethod Patch = new("PATCH"); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IIsRetryableContent.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IIsRetryableContent.cs new file mode 100644 index 000000000000..f546bd2184cf --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IIsRetryableContent.cs @@ -0,0 +1,6 @@ +namespace SeedSimpleApi.Core; + +public interface IIsRetryableContent +{ + public bool IsRetryable { get; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IRequestOptions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IRequestOptions.cs new file mode 100644 index 000000000000..3f729f2ae393 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/IRequestOptions.cs @@ -0,0 +1,83 @@ +namespace SeedSimpleApi.Core; + +internal interface IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with the request. + /// Headers previously set with matching keys will be overwritten. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The max number of retries to attempt. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional query parameters sent with the request. + /// + public IEnumerable> AdditionalQueryParameters { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional body properties sent with the request. + /// This is only applied to JSON requests. + /// + public object? AdditionalBodyProperties { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonAccessAttribute.cs new file mode 100644 index 000000000000..afe76ff95171 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,15 @@ +namespace SeedSimpleApi.Core; + +[global::System.AttributeUsage( + global::System.AttributeTargets.Property | global::System.AttributeTargets.Field +)] +internal class JsonAccessAttribute(JsonAccessType accessType) : global::System.Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonConfiguration.cs new file mode 100644 index 000000000000..e885a86965f2 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonConfiguration.cs @@ -0,0 +1,275 @@ +using global::System.Reflection; +using global::System.Text.Encodings.Web; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; + +namespace SeedSimpleApi.Core; + +internal static partial class JsonOptions +{ + internal static readonly JsonSerializerOptions JsonSerializerOptions; + internal static readonly JsonSerializerOptions JsonSerializerOptionsRelaxedEscaping; + + static JsonOptions() + { + var options = new JsonSerializerOptions + { + Converters = + { + new DateTimeSerializer(), +#if USE_PORTABLE_DATE_ONLY + new DateOnlyConverter(), +#endif + new OneOfSerializer(), + new OptionalJsonConverterFactory(), + }, +#if DEBUG + WriteIndented = true, +#endif + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + NullableOptionalModifier, + JsonAccessAndIgnoreModifier, + HandleExtensionDataFields, + }, + }, + }; + ConfigureJsonSerializerOptions(options); + JsonSerializerOptions = options; + + var relaxedOptions = new JsonSerializerOptions(options) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + JsonSerializerOptionsRelaxedEscaping = relaxedOptions; + } + + private static void NullableOptionalModifier(JsonTypeInfo typeInfo) + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var property in typeInfo.Properties) + { + var propertyInfo = property.AttributeProvider as global::System.Reflection.PropertyInfo; + + if (propertyInfo is null) + continue; + + // Check for ReadOnly JsonAccessAttribute - it overrides Optional/Nullable behavior + var jsonAccessAttribute = propertyInfo.GetCustomAttribute(); + if (jsonAccessAttribute?.AccessType == JsonAccessType.ReadOnly) + { + // ReadOnly means "never serialize", which completely overrides Optional/Nullable. + // Skip Optional/Nullable processing since JsonAccessAndIgnoreModifier + // will set ShouldSerialize = false anyway. + continue; + } + // Note: WriteOnly doesn't conflict with Optional/Nullable since it only + // affects deserialization (Set), not serialization (ShouldSerialize) + + var isOptionalType = + property.PropertyType.IsGenericType + && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>); + + var hasOptionalAttribute = + propertyInfo.GetCustomAttribute() is not null; + var hasNullableAttribute = + propertyInfo.GetCustomAttribute() is not null; + + if (isOptionalType && hasOptionalAttribute) + { + var originalGetter = property.Get; + if (originalGetter is not null) + { + var capturedIsNullable = hasNullableAttribute; + + property.ShouldSerialize = (obj, value) => + { + var optionalValue = originalGetter(obj); + if (optionalValue is not IOptional optional) + return false; + + if (!optional.IsDefined) + return false; + + if (!capturedIsNullable) + { + var innerValue = optional.GetBoxedValue(); + if (innerValue is null) + return false; + } + + return true; + }; + } + } + else if (hasNullableAttribute) + { + // Force serialization of nullable properties even when null + property.ShouldSerialize = (obj, value) => true; + } + } + } + + private static void JsonAccessAndIgnoreModifier(JsonTypeInfo typeInfo) + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes(typeof(JsonAccessAttribute), true) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute is not null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + var jsonIgnoreAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes(typeof(JsonIgnoreAttribute), true) + .OfType() + .FirstOrDefault(); + + if (jsonIgnoreAttribute is not null) + { + propertyInfo.IsRequired = false; + } + } + } + + private static void HandleExtensionDataFields(JsonTypeInfo typeInfo) + { + if ( + typeInfo.Kind == JsonTypeInfoKind.Object + && typeInfo.Properties.All(prop => !prop.IsExtensionData) + ) + { + var extensionProp = typeInfo + .Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic) + .FirstOrDefault(prop => + prop.GetCustomAttribute() is not null + ); + + if (extensionProp is not null) + { + var jsonPropertyInfo = typeInfo.CreateJsonPropertyInfo( + extensionProp.FieldType, + extensionProp.Name + ); + jsonPropertyInfo.Get = extensionProp.GetValue; + jsonPropertyInfo.Set = extensionProp.SetValue; + jsonPropertyInfo.IsExtensionData = true; + typeInfo.Properties.Add(jsonPropertyInfo); + } + } + } + + static partial void ConfigureJsonSerializerOptions(JsonSerializerOptions defaultOptions); +} + +internal static class JsonUtils +{ + internal static string Serialize(T obj) => + JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions); + + internal static string Serialize(object obj, global::System.Type type) => + JsonSerializer.Serialize(obj, type, JsonOptions.JsonSerializerOptions); + + internal static string SerializeRelaxedEscaping(T obj) => + JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptionsRelaxedEscaping); + + internal static string SerializeRelaxedEscaping(object obj, global::System.Type type) => + JsonSerializer.Serialize(obj, type, JsonOptions.JsonSerializerOptionsRelaxedEscaping); + + internal static JsonElement SerializeToElement(T obj) => + JsonSerializer.SerializeToElement(obj, JsonOptions.JsonSerializerOptions); + + internal static JsonElement SerializeToElement(object obj, global::System.Type type) => + JsonSerializer.SerializeToElement(obj, type, JsonOptions.JsonSerializerOptions); + + internal static JsonDocument SerializeToDocument(T obj) => + JsonSerializer.SerializeToDocument(obj, JsonOptions.JsonSerializerOptions); + + internal static JsonNode? SerializeToNode(T obj) => + JsonSerializer.SerializeToNode(obj, JsonOptions.JsonSerializerOptions); + + internal static byte[] SerializeToUtf8Bytes(T obj) => + JsonSerializer.SerializeToUtf8Bytes(obj, JsonOptions.JsonSerializerOptions); + + internal static string SerializeWithAdditionalProperties( + T obj, + object? additionalProperties = null + ) + { + if (additionalProperties is null) + { + return Serialize(obj); + } + var additionalPropertiesJsonNode = SerializeToNode(additionalProperties); + if (additionalPropertiesJsonNode is not JsonObject additionalPropertiesJsonObject) + { + throw new InvalidOperationException( + "The additional properties must serialize to a JSON object." + ); + } + var jsonNode = SerializeToNode(obj); + if (jsonNode is not JsonObject jsonObject) + { + throw new InvalidOperationException( + "The serialized object must be a JSON object to add properties." + ); + } + MergeJsonObjects(jsonObject, additionalPropertiesJsonObject); + return jsonObject.ToJsonString(JsonOptions.JsonSerializerOptions); + } + + private static void MergeJsonObjects(JsonObject baseObject, JsonObject overrideObject) + { + foreach (var property in overrideObject) + { + if (!baseObject.TryGetPropertyValue(property.Key, out JsonNode? existingValue)) + { + baseObject[property.Key] = property.Value is not null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + continue; + } + if ( + existingValue is JsonObject nestedBaseObject + && property.Value is JsonObject nestedOverrideObject + ) + { + // If both values are objects, recursively merge them. + MergeJsonObjects(nestedBaseObject, nestedOverrideObject); + continue; + } + // Otherwise, the overrideObject takes precedence. + baseObject[property.Key] = property.Value is not null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + } + } + + internal static T Deserialize(string json) => + JsonSerializer.Deserialize(json, JsonOptions.JsonSerializerOptions)!; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonRequest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonRequest.cs new file mode 100644 index 000000000000..b748d05cce42 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/JsonRequest.cs @@ -0,0 +1,36 @@ +using global::System.Net.Http; + +namespace SeedSimpleApi.Core; + +/// +/// The request object to be sent for JSON APIs. +/// +internal record JsonRequest : BaseRequest +{ + internal object? Body { get; init; } + + internal override HttpContent? CreateContent() + { + if (Body is null && Options?.AdditionalBodyProperties is null) + { + return null; + } + + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + ContentType, + Utf8NoBom, + "application/json" + ); + var content = new StringContent( + JsonUtils.SerializeWithAdditionalProperties(Body, Options?.AdditionalBodyProperties), + encoding, + mediaType + ); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + return content; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/MultipartFormRequest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/MultipartFormRequest.cs new file mode 100644 index 000000000000..80b87946adfc --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/MultipartFormRequest.cs @@ -0,0 +1,294 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; + +namespace SeedSimpleApi.Core; + +/// +/// The request object to be sent for multipart form data. +/// +internal record MultipartFormRequest : BaseRequest +{ + private readonly List> _partAdders = []; + + internal void AddJsonPart(string name, object? value) => AddJsonPart(name, value, null); + + internal void AddJsonPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + contentType, + Utf8NoBom, + "application/json" + ); + var content = new StringContent(JsonUtils.Serialize(value), encoding, mediaType); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + form.Add(content, name); + }); + } + + internal void AddJsonParts(string name, IEnumerable? value) => + AddJsonParts(name, value, null); + + internal void AddJsonParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddJsonPart(name, item, contentType); + } + } + + internal void AddJsonParts(string name, IEnumerable? value) => + AddJsonParts(name, value, null); + + internal void AddJsonParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddJsonPart(name, item, contentType); + } + } + + internal void AddStringPart(string name, object? value) => AddStringPart(name, value, null); + + internal void AddStringPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + AddStringPart(name, ValueConvert.ToString(value), contentType); + } + + internal void AddStringPart(string name, string? value) => AddStringPart(name, value, null); + + internal void AddStringPart(string name, string? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var (encoding, charset, mediaType) = ParseContentTypeOrDefault( + contentType, + Utf8NoBom, + "text/plain" + ); + var content = new StringContent(value, encoding, mediaType); + if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) + { + content.Headers.ContentType.CharSet = ""; + } + + form.Add(content, name); + }); + } + + internal void AddStringParts(string name, IEnumerable? value) => + AddStringParts(name, value, null); + + internal void AddStringParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + AddStringPart(name, ValueConvert.ToString(value), contentType); + } + + internal void AddStringParts(string name, IEnumerable? value) => + AddStringParts(name, value, null); + + internal void AddStringParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddStringPart(name, item, contentType); + } + } + + internal void AddStreamPart(string name, Stream? stream, string? fileName) => + AddStreamPart(name, stream, fileName, null); + + internal void AddStreamPart(string name, Stream? stream, string? fileName, string? contentType) + { + if (stream is null) + { + return; + } + + _partAdders.Add(form => + { + var content = new StreamContent(stream) + { + Headers = + { + ContentType = MediaTypeHeaderValue.Parse( + contentType ?? "application/octet-stream" + ), + }, + }; + + if (fileName is not null) + { + form.Add(content, name, fileName); + } + else + { + form.Add(content, name); + } + }); + } + + internal void AddFileParameterPart(string name, Stream? stream) => + AddStreamPart(name, stream, null, null); + + internal void AddFileParameterPart(string name, FileParameter? file) => + AddFileParameterPart(name, file, null); + + internal void AddFileParameterPart( + string name, + FileParameter? file, + string? fallbackContentType + ) => + AddStreamPart(name, file?.Stream, file?.FileName, file?.ContentType ?? fallbackContentType); + + internal void AddFileParameterParts(string name, IEnumerable? files) => + AddFileParameterParts(name, files, null); + + internal void AddFileParameterParts( + string name, + IEnumerable? files, + string? fallbackContentType + ) + { + if (files is null) + { + return; + } + + foreach (var file in files) + { + AddFileParameterPart(name, file, fallbackContentType); + } + } + + internal void AddFormEncodedPart(string name, object? value) => + AddFormEncodedPart(name, value, null); + + internal void AddFormEncodedPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var content = FormUrlEncoder.EncodeAsForm(value); + if (!string.IsNullOrEmpty(contentType)) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + form.Add(content, name); + }); + } + + internal void AddFormEncodedParts(string name, IEnumerable? value) => + AddFormEncodedParts(name, value, null); + + internal void AddFormEncodedParts(string name, IEnumerable? value, string? contentType) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddFormEncodedPart(name, item, contentType); + } + } + + internal void AddExplodedFormEncodedPart(string name, object? value) => + AddExplodedFormEncodedPart(name, value, null); + + internal void AddExplodedFormEncodedPart(string name, object? value, string? contentType) + { + if (value is null) + { + return; + } + + _partAdders.Add(form => + { + var content = FormUrlEncoder.EncodeAsExplodedForm(value); + if (!string.IsNullOrEmpty(contentType)) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + form.Add(content, name); + }); + } + + internal void AddExplodedFormEncodedParts(string name, IEnumerable? value) => + AddExplodedFormEncodedParts(name, value, null); + + internal void AddExplodedFormEncodedParts( + string name, + IEnumerable? value, + string? contentType + ) + { + if (value is null) + { + return; + } + + foreach (var item in value) + { + AddExplodedFormEncodedPart(name, item, contentType); + } + } + + internal override HttpContent CreateContent() + { + var form = new MultipartFormDataContent(); + foreach (var adder in _partAdders) + { + adder(form); + } + + return form; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/NullableAttribute.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/NullableAttribute.cs new file mode 100644 index 000000000000..cb2a8aafd8f8 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/NullableAttribute.cs @@ -0,0 +1,18 @@ +namespace SeedSimpleApi.Core; + +/// +/// Marks a property as nullable in the OpenAPI specification. +/// When applied to Optional properties, this indicates that null values should be +/// written to JSON when the optional is defined with null. +/// +/// +/// For regular (required) properties: +/// - Without [Nullable]: null values are invalid (omit from JSON at runtime) +/// - With [Nullable]: null values are written to JSON +/// +/// For Optional properties (also marked with [Optional]): +/// - Without [Nullable]: Optional.Of(null) → omit from JSON (runtime edge case) +/// - With [Nullable]: Optional.Of(null) → write null to JSON +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] +public class NullableAttribute : global::System.Attribute { } diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OneOfSerializer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OneOfSerializer.cs new file mode 100644 index 000000000000..b2223ce36511 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OneOfSerializer.cs @@ -0,0 +1,145 @@ +using global::System.Reflection; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using OneOf; + +namespace SeedSimpleApi.Core; + +internal class OneOfSerializer : JsonConverter +{ + public override IOneOf? Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType is JsonTokenType.Null) + return default; + + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + var readerCopy = reader; + var result = JsonSerializer.Deserialize(ref readerCopy, type, options); + reader.Skip(); + return (IOneOf)cast.Invoke(null, [result])!; + } + catch (JsonException) { } + } + + throw new JsonException( + $"Cannot deserialize into one of the supported types for {typeToConvert}" + ); + } + + public override void Write(Utf8JsonWriter writer, IOneOf value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.Value, options); + } + + public override IOneOf ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = reader.GetString(); + if (stringValue == null) + throw new JsonException("Cannot deserialize null property name into OneOf type"); + + // Try to deserialize the string value into one of the supported types + foreach (var (type, cast) in GetOneOfTypes(typeToConvert)) + { + try + { + // For primitive types, try direct conversion + if (type == typeof(string)) + { + return (IOneOf)cast.Invoke(null, [stringValue])!; + } + + // For other types, try to deserialize from JSON string + var result = JsonSerializer.Deserialize($"\"{stringValue}\"", type, options); + if (result != null) + { + return (IOneOf)cast.Invoke(null, [result])!; + } + } + catch { } + } + + // If no type-specific deserialization worked, default to string if available + var stringType = GetOneOfTypes(typeToConvert).FirstOrDefault(t => t.type == typeof(string)); + if (stringType != default) + { + return (IOneOf)stringType.cast.Invoke(null, [stringValue])!; + } + + throw new JsonException( + $"Cannot deserialize dictionary key '{stringValue}' into one of the supported types for {typeToConvert}" + ); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + IOneOf value, + JsonSerializerOptions options + ) + { + // Serialize the underlying value to a string suitable for use as a dictionary key + var stringValue = value.Value?.ToString() ?? "null"; + writer.WritePropertyName(stringValue); + } + + private static (global::System.Type type, MethodInfo cast)[] GetOneOfTypes( + global::System.Type typeToConvert + ) + { + var type = typeToConvert; + if (Nullable.GetUnderlyingType(type) is { } underlyingType) + { + type = underlyingType; + } + + var casts = type.GetRuntimeMethods() + .Where(m => m.IsSpecialName && m.Name == "op_Implicit") + .ToArray(); + while (type is not null) + { + if ( + type.IsGenericType + && (type.Name.StartsWith("OneOf`") || type.Name.StartsWith("OneOfBase`")) + ) + { + var genericArguments = type.GetGenericArguments(); + if (genericArguments.Length == 1) + { + return [(genericArguments[0], casts[0])]; + } + + // if object type is present, make sure it is last + var indexOfObjectType = Array.IndexOf(genericArguments, typeof(object)); + if (indexOfObjectType != -1 && genericArguments.Length - 1 != indexOfObjectType) + { + genericArguments = genericArguments + .OrderBy(t => t == typeof(object) ? 1 : 0) + .ToArray(); + } + + return genericArguments + .Select(t => (t, casts.First(c => c.GetParameters()[0].ParameterType == t))) + .ToArray(); + } + + type = type.BaseType; + } + + throw new InvalidOperationException($"{type} isn't OneOf or OneOfBase"); + } + + public override bool CanConvert(global::System.Type typeToConvert) + { + return typeof(IOneOf).IsAssignableFrom(typeToConvert); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Optional.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Optional.cs new file mode 100644 index 000000000000..3e2a2a8a0565 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Optional.cs @@ -0,0 +1,474 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedSimpleApi.Core; + +/// +/// Non-generic interface for Optional types to enable reflection-free checks. +/// +public interface IOptional +{ + /// + /// Returns true if the value is defined (set), even if the value is null. + /// + bool IsDefined { get; } + + /// + /// Gets the boxed value. Returns null if undefined or if the value is null. + /// + object? GetBoxedValue(); +} + +/// +/// Represents a field that can be "not set" (undefined) vs "explicitly set" (defined). +/// Use this for HTTP PATCH requests where you need to distinguish between: +/// +/// Undefined: Don't send this field (leave it unchanged on the server) +/// Defined with null: Send null (clear the field on the server) +/// Defined with value: Send the value (update the field on the server) +/// +/// +/// The type of the value. Use nullable types (T?) for fields that can be null. +/// +/// For nullable string fields, use Optional<string?>: +/// +/// public class UpdateUserRequest +/// { +/// public Optional<string?> Name { get; set; } = Optional<string?>.Undefined; +/// } +/// +/// var request = new UpdateUserRequest +/// { +/// Name = "John" // Will send: { "name": "John" } +/// }; +/// +/// var request2 = new UpdateUserRequest +/// { +/// Name = Optional<string?>.Of(null) // Will send: { "name": null } +/// }; +/// +/// var request3 = new UpdateUserRequest(); // Will send: {} (name not included) +/// +/// +public readonly struct Optional : IOptional, IEquatable> +{ + private readonly T _value; + private readonly bool _isDefined; + + private Optional(T value, bool isDefined) + { + _value = value; + _isDefined = isDefined; + } + + /// + /// Creates an undefined value - the field will not be included in the HTTP request. + /// Use this as the default value for optional fields. + /// + /// + /// + /// public Optional<string?> Email { get; set; } = Optional<string?>.Undefined; + /// + /// + public static Optional Undefined => new(default!, false); + + /// + /// Creates a defined value - the field will be included in the HTTP request. + /// The value can be null if T is a nullable type. + /// + /// The value to set. Can be null if T is nullable (e.g., string?, int?). + /// + /// + /// // Set to a value + /// request.Name = Optional<string?>.Of("John"); + /// + /// // Set to null (clears the field) + /// request.Email = Optional<string?>.Of(null); + /// + /// // Or use implicit conversion + /// request.Name = "John"; // Same as Of("John") + /// request.Email = null; // Same as Of(null) + /// + /// + public static Optional Of(T value) => new(value, true); + + /// + /// Returns true if the field is defined (set), even if the value is null. + /// Use this to determine if the field should be included in the HTTP request. + /// + /// + /// + /// if (request.Name.IsDefined) + /// { + /// requestBody["name"] = request.Name.Value; // Include in request (can be null) + /// } + /// + /// + public bool IsDefined => _isDefined; + + /// + /// Returns true if the field is undefined (not set). + /// Use this to check if the field should be excluded from the HTTP request. + /// + /// + /// + /// if (request.Email.IsUndefined) + /// { + /// // Don't include email in the request - leave it unchanged + /// } + /// + /// + public bool IsUndefined => !_isDefined; + + /// + /// Gets the value. The value may be null if T is a nullable type. + /// + /// Thrown if the value is undefined. + /// + /// Always check before accessing Value, or use instead. + /// + /// + /// + /// if (request.Name.IsDefined) + /// { + /// string? name = request.Name.Value; // Safe - can be null if Optional<string?> + /// } + /// + /// // Or check for null explicitly + /// if (request.Email.IsDefined && request.Email.Value is null) + /// { + /// // Email is explicitly set to null (clear it) + /// } + /// + /// + public T Value + { + get + { + if (!_isDefined) + throw new InvalidOperationException("Optional value is undefined"); + return _value; + } + } + + /// + /// Gets the value if defined, otherwise returns the specified default value. + /// Note: If the value is defined as null, this returns null (not the default). + /// + /// The value to return if undefined. + /// The actual value if defined (can be null), otherwise the default value. + /// + /// + /// string name = request.Name.GetValueOrDefault("Anonymous"); + /// // If Name is undefined: returns "Anonymous" + /// // If Name is Of(null): returns null + /// // If Name is Of("John"): returns "John" + /// + /// + public T GetValueOrDefault(T defaultValue = default!) + { + return _isDefined ? _value : defaultValue; + } + + /// + /// Tries to get the value. Returns true if the value is defined (even if null). + /// + /// + /// When this method returns, contains the value if defined, or default(T) if undefined. + /// The value may be null if T is nullable. + /// + /// True if the value is defined; otherwise, false. + /// + /// + /// if (request.Email.TryGetValue(out var email)) + /// { + /// requestBody["email"] = email; // email can be null + /// } + /// else + /// { + /// // Email is undefined - don't include in request + /// } + /// + /// + public bool TryGetValue(out T value) + { + if (_isDefined) + { + value = _value; + return true; + } + value = default!; + return false; + } + + /// + /// Implicitly converts a value to Optional<T>.Of(value). + /// This allows natural assignment: request.Name = "John" instead of request.Name = Optional<string?>.Of("John"). + /// + /// The value to convert (can be null if T is nullable). + public static implicit operator Optional(T value) => Of(value); + + /// + /// Returns a string representation of this Optional value. + /// + /// "Undefined" if not set, or "Defined(value)" if set. + public override string ToString() => _isDefined ? $"Defined({_value})" : "Undefined"; + + /// + /// Gets the boxed value. Returns null if undefined or if the value is null. + /// + public object? GetBoxedValue() + { + if (!_isDefined) + return null; + return _value; + } + + /// + public bool Equals(Optional other) => + _isDefined == other._isDefined && EqualityComparer.Default.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is Optional other && Equals(other); + + /// + public override int GetHashCode() + { + if (!_isDefined) + return 0; + unchecked + { + int hash = 17; + hash = hash * 31 + 1; // _isDefined = true + hash = hash * 31 + (_value is null ? 0 : _value.GetHashCode()); + return hash; + } + } + + /// + /// Determines whether two Optional values are equal. + /// + /// The first Optional to compare. + /// The second Optional to compare. + /// True if the Optional values are equal; otherwise, false. + public static bool operator ==(Optional left, Optional right) => left.Equals(right); + + /// + /// Determines whether two Optional values are not equal. + /// + /// The first Optional to compare. + /// The second Optional to compare. + /// True if the Optional values are not equal; otherwise, false. + public static bool operator !=(Optional left, Optional right) => !left.Equals(right); +} + +/// +/// Extension methods for Optional to simplify common operations. +/// +public static class OptionalExtensions +{ + /// + /// Adds the value to a dictionary if the optional is defined (even if the value is null). + /// This is useful for building JSON request payloads where null values should be included. + /// + /// The type of the optional value. + /// The optional value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Name.AddTo(dict, "name"); // Adds only if Name.IsDefined + /// request.Email.AddTo(dict, "email"); // Adds only if Email.IsDefined + /// + /// + public static void AddTo( + this Optional optional, + Dictionary dictionary, + string key + ) + { + if (optional.IsDefined) + { + dictionary[key] = optional.Value; + } + } + + /// + /// Executes an action if the optional is defined. + /// + /// The type of the optional value. + /// The optional value. + /// The action to execute with the value. + /// + /// + /// request.Name.IfDefined(name => Console.WriteLine($"Name: {name}")); + /// + /// + public static void IfDefined(this Optional optional, Action action) + { + if (optional.IsDefined) + { + action(optional.Value); + } + } + + /// + /// Maps the value to a new type if the optional is defined, otherwise returns undefined. + /// + /// The type of the original value. + /// The type to map to. + /// The optional value to map. + /// The mapping function. + /// An optional containing the mapped value if defined, otherwise undefined. + /// + /// + /// Optional<string?> name = Optional<string?>.Of("John"); + /// Optional<int> length = name.Map(n => n?.Length ?? 0); // Optional.Of(4) + /// + /// + public static Optional Map( + this Optional optional, + Func mapper + ) + { + return optional.IsDefined + ? Optional.Of(mapper(optional.Value)) + : Optional.Undefined; + } + + /// + /// Adds a nullable value to a dictionary only if it is not null. + /// This is useful for regular nullable properties where null means "omit from request". + /// + /// The type of the value (must be a reference type or Nullable). + /// The nullable value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Description.AddIfNotNull(dict, "description"); // Only adds if not null + /// request.Score.AddIfNotNull(dict, "score"); // Only adds if not null + /// + /// + public static void AddIfNotNull( + this T? value, + Dictionary dictionary, + string key + ) + where T : class + { + if (value is not null) + { + dictionary[key] = value; + } + } + + /// + /// Adds a nullable value type to a dictionary only if it has a value. + /// This is useful for regular nullable properties where null means "omit from request". + /// + /// The underlying value type. + /// The nullable value to add. + /// The dictionary to add to. + /// The key to use in the dictionary. + /// + /// + /// var dict = new Dictionary<string, object?>(); + /// request.Age.AddIfNotNull(dict, "age"); // Only adds if HasValue + /// request.Score.AddIfNotNull(dict, "score"); // Only adds if HasValue + /// + /// + public static void AddIfNotNull( + this T? value, + Dictionary dictionary, + string key + ) + where T : struct + { + if (value.HasValue) + { + dictionary[key] = value.Value; + } + } +} + +/// +/// JSON converter factory for Optional that handles undefined vs null correctly. +/// Uses a TypeInfoResolver to conditionally include/exclude properties based on Optional.IsDefined. +/// +public class OptionalJsonConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(global::System.Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + return false; + + return typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); + } + + public override JsonConverter? CreateConverter( + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var valueType = typeToConvert.GetGenericArguments()[0]; + var converterType = typeof(OptionalJsonConverter<>).MakeGenericType(valueType); + return (JsonConverter?)global::System.Activator.CreateInstance(converterType); + } +} + +/// +/// JSON converter for Optional that unwraps the value during serialization. +/// The actual property skipping is handled by the OptionalTypeInfoResolver. +/// +public class OptionalJsonConverter : JsonConverter> +{ + public override Optional Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType == JsonTokenType.Null) + { + return Optional.Of(default!); + } + + var value = JsonSerializer.Deserialize(ref reader, options); + return Optional.Of(value!); + } + + public override void Write( + Utf8JsonWriter writer, + Optional value, + JsonSerializerOptions options + ) + { + // This will be called by the serializer + // We need to unwrap and serialize the inner value + // The TypeInfoResolver will handle skipping undefined values + + if (value.IsUndefined) + { + // This shouldn't be called for undefined values due to ShouldSerialize + // But if it is, write null and let the resolver filter it + writer.WriteNullValue(); + return; + } + + // Get the inner value + var innerValue = value.Value; + + // Write null directly if the value is null (don't use JsonSerializer.Serialize for null) + if (innerValue is null) + { + writer.WriteNullValue(); + return; + } + + // Serialize the unwrapped value + JsonSerializer.Serialize(writer, innerValue, options); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OptionalAttribute.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OptionalAttribute.cs new file mode 100644 index 000000000000..08d8072360ef --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/OptionalAttribute.cs @@ -0,0 +1,17 @@ +namespace SeedSimpleApi.Core; + +/// +/// Marks a property as optional in the OpenAPI specification. +/// Optional properties use the Optional type and can be undefined (not present in JSON). +/// +/// +/// Properties marked with [Optional] should use the Optional type: +/// - Undefined: Optional.Undefined → omitted from JSON +/// - Defined: Optional.Of(value) → written to JSON +/// +/// Combine with [Nullable] to allow null values: +/// - [Optional, Nullable] Optional → can be undefined, null, or a value +/// - [Optional] Optional → can be undefined or a value (null is invalid) +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] +public class OptionalAttribute : global::System.Attribute { } diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/AdditionalProperties.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/AdditionalProperties.cs new file mode 100644 index 000000000000..f216280f593b --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/AdditionalProperties.cs @@ -0,0 +1,353 @@ +using global::System.Collections; +using global::System.Collections.ObjectModel; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi; + +public record ReadOnlyAdditionalProperties : ReadOnlyAdditionalProperties +{ + internal ReadOnlyAdditionalProperties() { } + + internal ReadOnlyAdditionalProperties(IDictionary properties) + : base(properties) { } +} + +public record ReadOnlyAdditionalProperties : IReadOnlyDictionary +{ + private readonly Dictionary _extensionData = new(); + private readonly Dictionary _convertedCache = new(); + + internal ReadOnlyAdditionalProperties() + { + _extensionData = new Dictionary(); + _convertedCache = new Dictionary(); + } + + internal ReadOnlyAdditionalProperties(IDictionary properties) + { + _extensionData = new Dictionary(properties.Count); + _convertedCache = new Dictionary(properties.Count); + foreach (var kvp in properties) + { + if (kvp.Value is JsonElement element) + { + _extensionData.Add(kvp.Key, element); + } + else + { + _extensionData[kvp.Key] = JsonUtils.SerializeToElement(kvp.Value); + } + + _convertedCache[kvp.Key] = kvp.Value; + } + } + + private static T ConvertToT(JsonElement value) + { + if (typeof(T) == typeof(JsonElement)) + { + return (T)(object)value; + } + + return value.Deserialize(JsonOptions.JsonSerializerOptions)!; + } + + internal void CopyFromExtensionData(IDictionary extensionData) + { + _extensionData.Clear(); + _convertedCache.Clear(); + foreach (var kvp in extensionData) + { + _extensionData[kvp.Key] = kvp.Value; + if (kvp.Value is T value) + { + _convertedCache[kvp.Key] = value; + } + } + } + + private T GetCached(string key) + { + if (_convertedCache.TryGetValue(key, out var cached)) + { + return cached; + } + + var value = ConvertToT(_extensionData[key]); + _convertedCache[key] = value; + return value; + } + + public IEnumerator> GetEnumerator() + { + return _extensionData + .Select(kvp => new KeyValuePair(kvp.Key, GetCached(kvp.Key))) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _extensionData.Count; + + public bool ContainsKey(string key) => _extensionData.ContainsKey(key); + + public bool TryGetValue(string key, out T value) + { + if (_convertedCache.TryGetValue(key, out value!)) + { + return true; + } + + if (_extensionData.TryGetValue(key, out var element)) + { + value = ConvertToT(element); + _convertedCache[key] = value; + return true; + } + + return false; + } + + public T this[string key] => GetCached(key); + + public IEnumerable Keys => _extensionData.Keys; + + public IEnumerable Values => Keys.Select(GetCached); +} + +public record AdditionalProperties : AdditionalProperties +{ + public AdditionalProperties() { } + + public AdditionalProperties(IDictionary properties) + : base(properties) { } +} + +public record AdditionalProperties : IDictionary +{ + private readonly Dictionary _extensionData; + private readonly Dictionary _convertedCache; + + public AdditionalProperties() + { + _extensionData = new Dictionary(); + _convertedCache = new Dictionary(); + } + + public AdditionalProperties(IDictionary properties) + { + _extensionData = new Dictionary(properties.Count); + _convertedCache = new Dictionary(properties.Count); + foreach (var kvp in properties) + { + _extensionData[kvp.Key] = kvp.Value; + _convertedCache[kvp.Key] = kvp.Value; + } + } + + private static T ConvertToT(object? extensionDataValue) + { + return extensionDataValue switch + { + T value => value, + JsonElement jsonElement => jsonElement.Deserialize( + JsonOptions.JsonSerializerOptions + )!, + JsonNode jsonNode => jsonNode.Deserialize(JsonOptions.JsonSerializerOptions)!, + _ => JsonUtils + .SerializeToElement(extensionDataValue) + .Deserialize(JsonOptions.JsonSerializerOptions)!, + }; + } + + internal void CopyFromExtensionData(IDictionary extensionData) + { + _extensionData.Clear(); + _convertedCache.Clear(); + foreach (var kvp in extensionData) + { + _extensionData[kvp.Key] = kvp.Value; + if (kvp.Value is T value) + { + _convertedCache[kvp.Key] = value; + } + } + } + + internal void CopyToExtensionData(IDictionary extensionData) + { + extensionData.Clear(); + foreach (var kvp in _extensionData) + { + extensionData[kvp.Key] = kvp.Value; + } + } + + public JsonObject ToJsonObject() => + ( + JsonUtils.SerializeToNode(_extensionData) + ?? throw new InvalidOperationException( + "Failed to serialize AdditionalProperties to JSON Node." + ) + ).AsObject(); + + public JsonNode ToJsonNode() => + JsonUtils.SerializeToNode(_extensionData) + ?? throw new InvalidOperationException( + "Failed to serialize AdditionalProperties to JSON Node." + ); + + public JsonElement ToJsonElement() => JsonUtils.SerializeToElement(_extensionData); + + public JsonDocument ToJsonDocument() => JsonUtils.SerializeToDocument(_extensionData); + + public IReadOnlyDictionary ToJsonElementDictionary() + { + return new ReadOnlyDictionary( + _extensionData.ToDictionary( + kvp => kvp.Key, + kvp => + { + if (kvp.Value is JsonElement jsonElement) + { + return jsonElement; + } + + return JsonUtils.SerializeToElement(kvp.Value); + } + ) + ); + } + + public ICollection Keys => _extensionData.Keys; + + public ICollection Values + { + get + { + var values = new T[_extensionData.Count]; + var i = 0; + foreach (var key in Keys) + { + values[i++] = GetCached(key); + } + + return values; + } + } + + private T GetCached(string key) + { + if (_convertedCache.TryGetValue(key, out var value)) + { + return value; + } + + value = ConvertToT(_extensionData[key]); + _convertedCache.Add(key, value); + return value; + } + + private void SetCached(string key, T value) + { + _extensionData[key] = value; + _convertedCache[key] = value; + } + + private void AddCached(string key, T value) + { + _extensionData.Add(key, value); + _convertedCache.Add(key, value); + } + + private bool RemoveCached(string key) + { + var isRemoved = _extensionData.Remove(key); + _convertedCache.Remove(key); + return isRemoved; + } + + public int Count => _extensionData.Count; + public bool IsReadOnly => false; + + public T this[string key] + { + get => GetCached(key); + set => SetCached(key, value); + } + + public void Add(string key, T value) => AddCached(key, value); + + public void Add(KeyValuePair item) => AddCached(item.Key, item.Value); + + public bool Remove(string key) => RemoveCached(key); + + public bool Remove(KeyValuePair item) => RemoveCached(item.Key); + + public bool ContainsKey(string key) => _extensionData.ContainsKey(key); + + public bool Contains(KeyValuePair item) + { + return _extensionData.ContainsKey(item.Key) + && EqualityComparer.Default.Equals(GetCached(item.Key), item.Value); + } + + public bool TryGetValue(string key, out T value) + { + if (_convertedCache.TryGetValue(key, out value!)) + { + return true; + } + + if (_extensionData.TryGetValue(key, out var extensionDataValue)) + { + value = ConvertToT(extensionDataValue); + _convertedCache[key] = value; + return true; + } + + return false; + } + + public void Clear() + { + _extensionData.Clear(); + _convertedCache.Clear(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (array.Length - arrayIndex < _extensionData.Count) + { + throw new ArgumentException( + "The array does not have enough space to copy the elements." + ); + } + + foreach (var kvp in _extensionData) + { + array[arrayIndex++] = new KeyValuePair(kvp.Key, GetCached(kvp.Key)); + } + } + + public IEnumerator> GetEnumerator() + { + return _extensionData + .Select(kvp => new KeyValuePair(kvp.Key, GetCached(kvp.Key))) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/ClientOptions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/ClientOptions.cs new file mode 100644 index 000000000000..cd0a3d3663d7 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/ClientOptions.cs @@ -0,0 +1,84 @@ +using SeedSimpleApi.Core; + +namespace SeedSimpleApi; + +[Serializable] +public partial class ClientOptions +{ + /// + /// The http headers sent with the request. + /// + internal Headers Headers { get; init; } = new(); + + /// + /// The Base URL for the API. + /// + public string BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = ""; + + /// + /// The http client used to make requests. + /// + public HttpClient HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new HttpClient(); + + /// + /// Additional headers to be sent with HTTP requests. + /// Headers with matching keys will be overwritten by headers set on the request. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = []; + + /// + /// The max number of retries to attempt. + /// + public int MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = 2; + + /// + /// The timeout for the request. + /// + public TimeSpan Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = TimeSpan.FromSeconds(30); + + /// + /// Clones this and returns a new instance + /// + internal ClientOptions Clone() + { + return new ClientOptions + { + BaseUrl = BaseUrl, + HttpClient = HttpClient, + MaxRetries = MaxRetries, + Timeout = Timeout, + Headers = new Headers(new Dictionary(Headers)), + AdditionalHeaders = AdditionalHeaders, + }; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/FileParameter.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/FileParameter.cs new file mode 100644 index 000000000000..c9fcda6aed6a --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/FileParameter.cs @@ -0,0 +1,63 @@ +namespace SeedSimpleApi; + +/// +/// File parameter for uploading files. +/// +public record FileParameter : IDisposable +#if NET6_0_OR_GREATER + , IAsyncDisposable +#endif +{ + private bool _disposed; + + /// + /// The name of the file to be uploaded. + /// + public string? FileName { get; set; } + + /// + /// The content type of the file to be uploaded. + /// + public string? ContentType { get; set; } + + /// + /// The content of the file to be uploaded. + /// + public required Stream Stream { get; set; } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + if (disposing) + { + Stream.Dispose(); + } + + _disposed = true; + } + +#if NET6_0_OR_GREATER + /// + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + await Stream.DisposeAsync().ConfigureAwait(false); + _disposed = true; + } + + GC.SuppressFinalize(this); + } +#endif + + public static implicit operator FileParameter(Stream stream) => new() { Stream = stream }; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RawResponse.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RawResponse.cs new file mode 100644 index 000000000000..a379a08844f3 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RawResponse.cs @@ -0,0 +1,24 @@ +using global::System.Net; + +namespace SeedSimpleApi; + +/// +/// Contains HTTP response metadata including status code, URL, and headers. +/// +public record RawResponse +{ + /// + /// The HTTP status code of the response. + /// + public required HttpStatusCode StatusCode { get; init; } + + /// + /// The request URL that generated this response. + /// + public required Uri Url { get; init; } + + /// + /// The HTTP response headers. + /// + public required Core.ResponseHeaders Headers { get; init; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RequestOptions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RequestOptions.cs new file mode 100644 index 000000000000..83399d0508cf --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/RequestOptions.cs @@ -0,0 +1,86 @@ +using SeedSimpleApi.Core; + +namespace SeedSimpleApi; + +[Serializable] +public partial class RequestOptions : IRequestOptions +{ + /// + /// The Base URL for the API. + /// + public string? BaseUrl { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The http client used to make requests. + /// + public HttpClient? HttpClient { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional headers to be sent with the request. + /// Headers previously set with matching keys will be overwritten. + /// + public IEnumerable> AdditionalHeaders { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = []; + + /// + /// The max number of retries to attempt. + /// + public int? MaxRetries { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// The timeout for the request. + /// + public TimeSpan? Timeout { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } + + /// + /// Additional query parameters sent with the request. + /// + public IEnumerable> AdditionalQueryParameters { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = Enumerable.Empty>(); + + /// + /// Additional body properties sent with the request. + /// This is only applied to JSON requests. + /// + public object? AdditionalBodyProperties { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiApiException.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiApiException.cs new file mode 100644 index 000000000000..756941505567 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiApiException.cs @@ -0,0 +1,22 @@ +namespace SeedSimpleApi; + +/// +/// This exception type will be thrown for any non-2XX API responses. +/// +public class SeedSimpleApiApiException( + string message, + int statusCode, + object body, + Exception? innerException = null +) : SeedSimpleApiException(message, innerException) +{ + /// + /// The error code of the response that triggered the exception. + /// + public int StatusCode => statusCode; + + /// + /// The body of the response that triggered the exception. + /// + public object Body => body; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiEnvironment.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiEnvironment.cs new file mode 100644 index 000000000000..236cb2fde6d9 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiEnvironment.cs @@ -0,0 +1,9 @@ +namespace SeedSimpleApi; + +[Serializable] +public class SeedSimpleApiEnvironment +{ + public const string Production = "https://api.example.com"; + + public const string Staging = "https://staging-api.example.com"; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiException.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiException.cs new file mode 100644 index 000000000000..09e061f18fc5 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/SeedSimpleApiException.cs @@ -0,0 +1,7 @@ +namespace SeedSimpleApi; + +/// +/// Base exception class for all exceptions thrown by the SDK. +/// +public class SeedSimpleApiException(string message, Exception? innerException = null) + : Exception(message, innerException); diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/Version.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/Version.cs new file mode 100644 index 000000000000..a0e32a1fffad --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/Version.cs @@ -0,0 +1,7 @@ +namespace SeedSimpleApi; + +[Serializable] +internal class Version +{ + public const string Current = "0.0.1"; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponse.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponse.cs new file mode 100644 index 000000000000..1adf4631fa8c --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponse.cs @@ -0,0 +1,18 @@ +namespace SeedSimpleApi; + +/// +/// Wraps a parsed response value with its raw HTTP response metadata. +/// +/// The type of the parsed response data. +public readonly struct WithRawResponse +{ + /// + /// The parsed response data. + /// + public required T Data { get; init; } + + /// + /// The raw HTTP response metadata. + /// + public required RawResponse RawResponse { get; init; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponseTask.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponseTask.cs new file mode 100644 index 000000000000..44b9144b3c4b --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/Public/WithRawResponseTask.cs @@ -0,0 +1,144 @@ +using global::System.Runtime.CompilerServices; + +namespace SeedSimpleApi; + +/// +/// A task-like type that wraps Task<WithRawResponse<T>> and provides dual-mode awaiting: +/// - Direct await yields just T (zero-allocation path for common case) +/// - .WithRawResponse() yields WithRawResponse<T> (when raw response metadata is needed) +/// +/// The type of the parsed response data. +public readonly struct WithRawResponseTask +{ + private readonly global::System.Threading.Tasks.Task> _task; + + /// + /// Creates a new WithRawResponseTask wrapping the given task. + /// + public WithRawResponseTask(global::System.Threading.Tasks.Task> task) + { + _task = task; + } + + /// + /// Returns the underlying task that yields both the data and raw response metadata. + /// + public global::System.Threading.Tasks.Task> WithRawResponse() => _task; + + /// + /// Gets the custom awaiter that unwraps to just T when awaited. + /// + public Awaiter GetAwaiter() => new(_task.GetAwaiter()); + + /// + /// Configures the awaiter to continue on the captured context or not. + /// + public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => + new(_task.ConfigureAwait(continueOnCapturedContext)); + + /// + /// Implicitly converts WithRawResponseTask<T> to global::System.Threading.Tasks.Task<T> for backward compatibility. + /// The resulting task will yield just the data when awaited. + /// + public static implicit operator global::System.Threading.Tasks.Task( + WithRawResponseTask task + ) + { + return task._task.ContinueWith( + t => t.Result.Data, + TaskContinuationOptions.ExecuteSynchronously + ); + } + + /// + /// Custom awaiter that unwraps WithRawResponse<T> to just T. + /// + public readonly struct Awaiter : ICriticalNotifyCompletion + { + private readonly TaskAwaiter> _awaiter; + + internal Awaiter(TaskAwaiter> awaiter) + { + _awaiter = awaiter; + } + + /// + /// Gets whether the underlying task has completed. + /// + public bool IsCompleted => _awaiter.IsCompleted; + + /// + /// Gets the result, unwrapping to just the data. + /// + public T GetResult() => _awaiter.GetResult().Data; + + /// + /// Schedules the continuation action. + /// + public void OnCompleted(global::System.Action continuation) => + _awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action without capturing the execution context. + /// + public void UnsafeOnCompleted(global::System.Action continuation) => + _awaiter.UnsafeOnCompleted(continuation); + } + + /// + /// Awaitable type returned by ConfigureAwait that unwraps to just T. + /// + public readonly struct ConfiguredTaskAwaitable + { + private readonly ConfiguredTaskAwaitable> _configuredTask; + + internal ConfiguredTaskAwaitable(ConfiguredTaskAwaitable> configuredTask) + { + _configuredTask = configuredTask; + } + + /// + /// Gets the configured awaiter that unwraps to just T. + /// + public ConfiguredAwaiter GetAwaiter() => new(_configuredTask.GetAwaiter()); + + /// + /// Custom configured awaiter that unwraps WithRawResponse<T> to just T. + /// + public readonly struct ConfiguredAwaiter : ICriticalNotifyCompletion + { + private readonly ConfiguredTaskAwaitable< + WithRawResponse + >.ConfiguredTaskAwaiter _awaiter; + + internal ConfiguredAwaiter( + ConfiguredTaskAwaitable>.ConfiguredTaskAwaiter awaiter + ) + { + _awaiter = awaiter; + } + + /// + /// Gets whether the underlying task has completed. + /// + public bool IsCompleted => _awaiter.IsCompleted; + + /// + /// Gets the result, unwrapping to just the data. + /// + public T GetResult() => _awaiter.GetResult().Data; + + /// + /// Schedules the continuation action. + /// + public void OnCompleted(global::System.Action continuation) => + _awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action without capturing the execution context. + /// + public void UnsafeOnCompleted(global::System.Action continuation) => + _awaiter.UnsafeOnCompleted(continuation); + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringBuilder.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringBuilder.cs new file mode 100644 index 000000000000..22ed6a31c0b6 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringBuilder.cs @@ -0,0 +1,484 @@ +using global::System.Buffers; +using global::System.Runtime.CompilerServices; +#if !NET6_0_OR_GREATER +using global::System.Text; +#endif + +namespace SeedSimpleApi.Core; + +/// +/// High-performance query string builder with cross-platform optimizations. +/// Uses span-based APIs on .NET 6+ and StringBuilder fallback for older targets. +/// +internal static class QueryStringBuilder +{ +#if NET8_0_OR_GREATER + private static readonly SearchValues UnreservedChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~" + ); +#else + private const string UnreservedChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"; +#endif + +#if NET7_0_OR_GREATER + private static ReadOnlySpan UpperHexChars => "0123456789ABCDEF"u8; +#else + private static readonly byte[] UpperHexChars = + { + (byte)'0', + (byte)'1', + (byte)'2', + (byte)'3', + (byte)'4', + (byte)'5', + (byte)'6', + (byte)'7', + (byte)'8', + (byte)'9', + (byte)'A', + (byte)'B', + (byte)'C', + (byte)'D', + (byte)'E', + (byte)'F', + }; +#endif + + /// + /// Builds a query string from the provided parameters. + /// +#if NET6_0_OR_GREATER + public static string Build(ReadOnlySpan> parameters) + { + if (parameters.IsEmpty) + return string.Empty; + + var estimatedLength = EstimateLength(parameters); + if (estimatedLength == 0) + return string.Empty; + + var bufferSize = Math.Min(estimatedLength * 3, 8192); + var buffer = ArrayPool.Shared.Rent(bufferSize); + + try + { + var written = BuildCore(parameters, buffer); + return new string(buffer.AsSpan(0, written)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private static int EstimateLength(ReadOnlySpan> parameters) + { + var estimatedLength = 0; + foreach (var kvp in parameters) + { + estimatedLength += kvp.Key.Length + kvp.Value.Length + 2; + } + return estimatedLength; + } +#endif + + /// + /// Builds a query string from the provided parameters. + /// + public static string Build(IEnumerable> parameters) + { +#if NET6_0_OR_GREATER + // Try to get span access for collections that support it + if (parameters is ICollection> collection) + { + if (collection.Count == 0) + return string.Empty; + + var array = ArrayPool>.Shared.Rent(collection.Count); + try + { + collection.CopyTo(array, 0); + return Build(array.AsSpan(0, collection.Count)); + } + finally + { + ArrayPool>.Shared.Return(array); + } + } + + // Fallback for non-collection enumerables + using var enumerator = parameters.GetEnumerator(); + if (!enumerator.MoveNext()) + return string.Empty; + + var buffer = ArrayPool.Shared.Rent(4096); + try + { + var position = 0; + var first = true; + + do + { + var kvp = enumerator.Current; + + // Ensure capacity (worst case: 3x for encoding + separators) + var required = (kvp.Key.Length + kvp.Value.Length + 2) * 3; + if (position + required > buffer.Length) + { + var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2); + buffer.AsSpan(0, position).CopyTo(newBuffer); + ArrayPool.Shared.Return(buffer); + buffer = newBuffer; + } + + buffer[position++] = first ? '?' : '&'; + first = false; + + position += EncodeComponent(kvp.Key.AsSpan(), buffer.AsSpan(position)); + buffer[position++] = '='; + position += EncodeComponent(kvp.Value.AsSpan(), buffer.AsSpan(position)); + } while (enumerator.MoveNext()); + + return first ? string.Empty : new string(buffer.AsSpan(0, position)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } +#else + // netstandard2.0 / net462 fallback using StringBuilder + var sb = new StringBuilder(); + var first = true; + + foreach (var kvp in parameters) + { + sb.Append(first ? '?' : '&'); + first = false; + + AppendEncoded(sb, kvp.Key); + sb.Append('='); + AppendEncoded(sb, kvp.Value); + } + + return sb.ToString(); +#endif + } + +#if NET6_0_OR_GREATER + private static int BuildCore( + ReadOnlySpan> parameters, + Span buffer + ) + { + var position = 0; + var first = true; + + foreach (var kvp in parameters) + { + buffer[position++] = first ? '?' : '&'; + first = false; + + position += EncodeComponent(kvp.Key.AsSpan(), buffer.Slice(position)); + buffer[position++] = '='; + position += EncodeComponent(kvp.Value.AsSpan(), buffer.Slice(position)); + } + + return position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EncodeComponent(ReadOnlySpan input, Span output) + { + if (!NeedsEncoding(input)) + { + input.CopyTo(output); + return input.Length; + } + + return EncodeSlow(input, output); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NeedsEncoding(ReadOnlySpan value) + { +#if NET8_0_OR_GREATER + return value.ContainsAnyExcept(UnreservedChars); +#else + foreach (var c in value) + { + if (!IsUnreserved(c)) + return true; + } + return false; +#endif + } + + private static int EncodeSlow(ReadOnlySpan input, Span output) + { + var position = 0; + + foreach (var c in input) + { + if (IsUnreserved(c)) + { + output[position++] = c; + } + else if (c == ' ') + { + output[position++] = '%'; + output[position++] = '2'; + output[position++] = '0'; + } +#if NET7_0_OR_GREATER + else if (char.IsAscii(c)) +#else + else if (c <= 127) +#endif + { + position += EncodeAscii((byte)c, output.Slice(position)); + } + else + { + position += EncodeUtf8(c, output.Slice(position)); + } + } + + return position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int EncodeAscii(byte value, Span output) + { + output[0] = '%'; + output[1] = (char)UpperHexChars[value >> 4]; + output[2] = (char)UpperHexChars[value & 0xF]; + return 3; + } + + private static int EncodeUtf8(char c, Span output) + { + Span utf8Bytes = stackalloc byte[4]; + Span singleChar = stackalloc char[1] { c }; + var byteCount = global::System.Text.Encoding.UTF8.GetBytes(singleChar, utf8Bytes); + + var position = 0; + for (var i = 0; i < byteCount; i++) + { + output[position++] = '%'; + output[position++] = (char)UpperHexChars[utf8Bytes[i] >> 4]; + output[position++] = (char)UpperHexChars[utf8Bytes[i] & 0xF]; + } + + return position; + } +#else + // netstandard2.0 / net462 StringBuilder-based encoding + private static void AppendEncoded(StringBuilder sb, string value) + { + foreach (var c in value) + { + if (IsUnreserved(c)) + { + sb.Append(c); + } + else if (c == ' ') + { + sb.Append("%20"); + } + else if (c <= 127) + { + AppendPercentEncoded(sb, (byte)c); + } + else + { + var bytes = Encoding.UTF8.GetBytes(new[] { c }); + foreach (var b in bytes) + { + AppendPercentEncoded(sb, b); + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendPercentEncoded(StringBuilder sb, byte value) + { + sb.Append('%'); + sb.Append((char)UpperHexChars[value >> 4]); + sb.Append((char)UpperHexChars[value & 0xF]); + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsUnreserved(char c) + { +#if NET8_0_OR_GREATER + return UnreservedChars.Contains(c); +#elif NET7_0_OR_GREATER + return char.IsAsciiLetterOrDigit(c) || c is '-' or '_' or '.' or '~'; +#else + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' + || c == '_' + || c == '.' + || c == '~'; +#endif + } + + /// + /// Fluent builder for constructing query strings with support for simple parameters and deep object notation. + /// + public sealed class Builder + { + private readonly List> _params; + + /// + /// Initializes a new instance with default capacity. + /// + public Builder() + { + _params = new List>(); + } + + /// + /// Initializes a new instance with the specified initial capacity. + /// + public Builder(int capacity) + { + _params = new List>(capacity); + } + + /// + /// Adds a simple parameter. For collections, adds multiple key-value pairs (one per element). + /// + public Builder Add(string key, object? value) + { + if (value is null) + { + return this; + } + + // Handle string separately since it implements IEnumerable + if (value is string stringValue) + { + _params.Add(new KeyValuePair(key, stringValue)); + return this; + } + + // Handle collections (arrays, lists, etc.) - add each element as a separate key-value pair + if ( + value + is global::System.Collections.IEnumerable enumerable + and not global::System.Collections.IDictionary + ) + { + foreach (var item in enumerable) + { + if (item is not null) + { + _params.Add( + new KeyValuePair( + key, + ValueConvert.ToQueryStringValue(item) + ) + ); + } + } + return this; + } + + // Handle scalar values + _params.Add( + new KeyValuePair(key, ValueConvert.ToQueryStringValue(value)) + ); + return this; + } + + /// + /// Sets a parameter, removing any existing parameters with the same key before adding the new value. + /// For collections, removes all existing parameters with the key, then adds multiple key-value pairs (one per element). + /// This allows overriding parameters set earlier in the builder. + /// + public Builder Set(string key, object? value) + { + // Remove all existing parameters with this key + _params.RemoveAll(kv => kv.Key == key); + + // Add the new value(s) + return Add(key, value); + } + + /// + /// Merges additional query parameters with override semantics. + /// Groups parameters by key and calls Set() once per unique key. + /// This ensures that parameters with the same key are properly merged: + /// - If a key appears once, it's added as a single value + /// - If a key appears multiple times, all values are added as an array + /// - All parameters override any existing parameters with the same key + /// + public Builder MergeAdditional( + global::System.Collections.Generic.IEnumerable>? additionalParameters + ) + { + if (additionalParameters is null) + { + return this; + } + + // Group by key to handle multiple values for the same key correctly + var grouped = additionalParameters + .GroupBy(kv => kv.Key) + .Select(g => new global::System.Collections.Generic.KeyValuePair( + g.Key, + g.Count() == 1 ? (object)g.First().Value : g.Select(kv => kv.Value).ToArray() + )); + + foreach (var param in grouped) + { + Set(param.Key, param.Value); + } + + return this; + } + + /// + /// Adds a complex object using deep object notation with a prefix. + /// Deep object notation nests properties with brackets: prefix[key][nested]=value + /// + public Builder AddDeepObject(string prefix, object? value) + { + if (value is not null) + { + _params.AddRange(QueryStringConverter.ToDeepObject(prefix, value)); + } + return this; + } + + /// + /// Adds a complex object using exploded form notation with an optional prefix. + /// Exploded form flattens properties: prefix[key]=value (no deep nesting). + /// + public Builder AddExploded(string prefix, object? value) + { + if (value is not null) + { + _params.AddRange(QueryStringConverter.ToExplodedForm(prefix, value)); + } + return this; + } + + /// + /// Builds the final query string. + /// + public string Build() + { + return QueryStringBuilder.Build(_params); + } + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringConverter.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringConverter.cs new file mode 100644 index 000000000000..c82a5097c528 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/QueryStringConverter.cs @@ -0,0 +1,259 @@ +using global::System.Text.Json; + +namespace SeedSimpleApi.Core; + +/// +/// Converts an object into a query string collection. +/// +internal static class QueryStringConverter +{ + /// + /// Converts an object into a query string collection using Deep Object notation with a prefix. + /// + /// The prefix to prepend to all keys (e.g., "session_settings"). Pass empty string for no prefix. + /// Object to form URL-encode. Can be an object, array of objects, or dictionary. + /// Throws when passing in a string or primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToDeepObject( + string prefix, + object value + ) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + JsonToDeepObject(json, prefix, queryCollection); + return queryCollection; + } + + /// + /// Converts an object into a query string collection using Deep Object notation. + /// + /// Object to form URL-encode. Can be an object, array of objects, or dictionary. + /// Throws when passing in a string or primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToDeepObject(object value) + { + return ToDeepObject("", value); + } + + /// + /// Converts an object into a query string collection using Exploded Form notation with a prefix. + /// + /// The prefix to prepend to all keys. Pass empty string for no prefix. + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToExplodedForm( + string prefix, + object value + ) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + AssertRootJson(json); + JsonToFormExploded(json, prefix, queryCollection); + return queryCollection; + } + + /// + /// Converts an object into a query string collection using Exploded Form notation. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToExplodedForm(object value) + { + return ToExplodedForm("", value); + } + + /// + /// Converts an object into a query string collection using Form notation without exploding parameters. + /// + /// Object to form URL-encode. You can pass in an object or dictionary, but not lists, strings, or primitives. + /// Throws when passing in a list, a string, or a primitive value. + /// A collection of key value pairs. The keys and values are not URL encoded. + internal static IEnumerable> ToForm(object value) + { + var queryCollection = new List>(); + var json = JsonUtils.SerializeToElement(value); + AssertRootJson(json); + JsonToForm(json, "", queryCollection); + return queryCollection; + } + + private static void AssertRootJson(JsonElement json) + { + switch (json.ValueKind) + { + case JsonValueKind.Object: + break; + case JsonValueKind.Array: + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + default: + throw new global::System.Exception( + $"Only objects can be converted to query string collections. Given type is {json.ValueKind}." + ); + } + } + + private static void JsonToForm( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToForm(property.Value, newPrefix, parameters); + } + break; + case JsonValueKind.Array: + var arrayValues = element.EnumerateArray().Select(ValueToString).ToArray(); + parameters.Add( + new KeyValuePair(prefix, string.Join(",", arrayValues)) + ); + break; + case JsonValueKind.Null: + break; + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static void JsonToFormExploded( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToFormExploded(property.Value, newPrefix, parameters); + } + + break; + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + if ( + item.ValueKind != JsonValueKind.Object + && item.ValueKind != JsonValueKind.Array + ) + { + parameters.Add( + new KeyValuePair(prefix, ValueToString(item)) + ); + } + else + { + JsonToFormExploded(item, prefix, parameters); + } + } + + break; + case JsonValueKind.Null: + break; + case JsonValueKind.Undefined: + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static void JsonToDeepObject( + JsonElement element, + string prefix, + List> parameters + ) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPrefix = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}[{property.Name}]"; + + JsonToDeepObject(property.Value, newPrefix, parameters); + } + + break; + case JsonValueKind.Array: + var index = 0; + foreach (var item in element.EnumerateArray()) + { + var newPrefix = $"{prefix}[{index++}]"; + + if ( + item.ValueKind != JsonValueKind.Object + && item.ValueKind != JsonValueKind.Array + ) + { + parameters.Add( + new KeyValuePair(newPrefix, ValueToString(item)) + ); + } + else + { + JsonToDeepObject(item, newPrefix, parameters); + } + } + + break; + case JsonValueKind.Null: + case JsonValueKind.Undefined: + // Skip null and undefined values - don't add parameters for them + break; + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + default: + parameters.Add(new KeyValuePair(prefix, ValueToString(element))); + break; + } + } + + private static string ValueToString(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString() ?? "", + JsonValueKind.Number => element.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + JsonValueKind.Null => "", + _ => element.GetRawText(), + }; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawClient.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawClient.cs new file mode 100644 index 000000000000..54b6cca832e0 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawClient.cs @@ -0,0 +1,344 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; +using global::System.Text; +using SystemTask = global::System.Threading.Tasks.Task; + +namespace SeedSimpleApi.Core; + +/// +/// Utility class for making raw HTTP requests to the API. +/// +internal partial class RawClient(ClientOptions clientOptions) +{ + private const int MaxRetryDelayMs = 60000; + private const double JitterFactor = 0.2; +#if NET6_0_OR_GREATER + // Use Random.Shared for thread-safe random number generation on .NET 6+ +#else + private static readonly object JitterLock = new(); + private static readonly Random JitterRandom = new(); +#endif + internal int BaseRetryDelay { get; set; } = 1000; + + /// + /// The client options applied on every request. + /// + internal readonly ClientOptions Options = clientOptions; + + internal async global::System.Threading.Tasks.Task SendRequestAsync( + global::SeedSimpleApi.Core.BaseRequest request, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = request.Options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + var httpRequest = await CreateHttpRequestAsync(request).ConfigureAwait(false); + // Send the request. + return await SendWithRetriesAsync(httpRequest, request.Options, cts.Token) + .ConfigureAwait(false); + } + + internal async global::System.Threading.Tasks.Task SendRequestAsync( + HttpRequestMessage request, + IRequestOptions? options, + CancellationToken cancellationToken = default + ) + { + // Apply the request timeout. + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeout = options?.Timeout ?? Options.Timeout; + cts.CancelAfter(timeout); + + // Send the request. + return await SendWithRetriesAsync(request, options, cts.Token).ConfigureAwait(false); + } + + private static async global::System.Threading.Tasks.Task CloneRequestAsync( + HttpRequestMessage request + ) + { + var clonedRequest = new HttpRequestMessage(request.Method, request.RequestUri); + clonedRequest.Version = request.Version; + + if (request.Content != null) + { + switch (request.Content) + { + case MultipartContent oldMultipartFormContent: + var originalBoundary = + oldMultipartFormContent + .Headers.ContentType?.Parameters.First(p => + p.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase) + ) + .Value?.Trim('"') + ?? Guid.NewGuid().ToString(); + var newMultipartContent = oldMultipartFormContent switch + { + MultipartFormDataContent => new MultipartFormDataContent(originalBoundary), + _ => new MultipartContent(), + }; + foreach (var content in oldMultipartFormContent) + { + var ms = new MemoryStream(); + await content.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + var newPart = new StreamContent(ms); + foreach (var header in content.Headers) + { + newPart.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + newMultipartContent.Add(newPart); + } + + clonedRequest.Content = newMultipartContent; + break; + default: + var bodyStream = new MemoryStream(); + await request.Content.CopyToAsync(bodyStream).ConfigureAwait(false); + bodyStream.Position = 0; + var clonedContent = new StreamContent(bodyStream); + foreach (var header in request.Content.Headers) + { + clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + clonedRequest.Content = clonedContent; + break; + } + } + + foreach (var header in request.Headers) + { + clonedRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clonedRequest; + } + + /// + /// Sends the request with retries, unless the request content is not retryable, + /// such as stream requests and multipart form data with stream content. + /// + private async global::System.Threading.Tasks.Task SendWithRetriesAsync( + HttpRequestMessage request, + IRequestOptions? options, + CancellationToken cancellationToken + ) + { + var httpClient = options?.HttpClient ?? Options.HttpClient; + var maxRetries = options?.MaxRetries ?? Options.MaxRetries; + var response = await httpClient + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + var isRetryableContent = IsRetryableContent(request); + + if (!isRetryableContent) + { + return new global::SeedSimpleApi.Core.ApiResponse + { + StatusCode = (int)response.StatusCode, + Raw = response, + }; + } + + for (var i = 0; i < maxRetries; i++) + { + if (!ShouldRetry(response)) + { + break; + } + + var delayMs = GetRetryDelayFromHeaders(response, i); + await SystemTask.Delay(delayMs, cancellationToken).ConfigureAwait(false); + using var retryRequest = await CloneRequestAsync(request).ConfigureAwait(false); + response = await httpClient + .SendAsync( + retryRequest, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken + ) + .ConfigureAwait(false); + } + + return new global::SeedSimpleApi.Core.ApiResponse + { + StatusCode = (int)response.StatusCode, + Raw = response, + }; + } + + private static bool ShouldRetry(HttpResponseMessage response) + { + var statusCode = (int)response.StatusCode; + return statusCode is 408 or 429 or >= 500; + } + + private static int AddPositiveJitter(int delayMs) + { +#if NET6_0_OR_GREATER + var random = Random.Shared.NextDouble(); +#else + double random; + lock (JitterLock) + { + random = JitterRandom.NextDouble(); + } +#endif + var jitterMultiplier = 1 + random * JitterFactor; + return (int)(delayMs * jitterMultiplier); + } + + private static int AddSymmetricJitter(int delayMs) + { +#if NET6_0_OR_GREATER + var random = Random.Shared.NextDouble(); +#else + double random; + lock (JitterLock) + { + random = JitterRandom.NextDouble(); + } +#endif + var jitterMultiplier = 1 + (random - 0.5) * JitterFactor; + return (int)(delayMs * jitterMultiplier); + } + + private int GetRetryDelayFromHeaders(HttpResponseMessage response, int retryAttempt) + { + if (response.Headers.TryGetValues("Retry-After", out var retryAfterValues)) + { + var retryAfter = retryAfterValues.FirstOrDefault(); + if (!string.IsNullOrEmpty(retryAfter)) + { + if (int.TryParse(retryAfter, out var retryAfterSeconds) && retryAfterSeconds > 0) + { + return Math.Min(retryAfterSeconds * 1000, MaxRetryDelayMs); + } + + if (DateTimeOffset.TryParse(retryAfter, out var retryAfterDate)) + { + var delay = (int)(retryAfterDate - DateTimeOffset.UtcNow).TotalMilliseconds; + if (delay > 0) + { + return Math.Min(delay, MaxRetryDelayMs); + } + } + } + } + + if (response.Headers.TryGetValues("X-RateLimit-Reset", out var rateLimitResetValues)) + { + var rateLimitReset = rateLimitResetValues.FirstOrDefault(); + if ( + !string.IsNullOrEmpty(rateLimitReset) + && long.TryParse(rateLimitReset, out var resetTime) + ) + { + var resetDateTime = DateTimeOffset.FromUnixTimeSeconds(resetTime); + var delay = (int)(resetDateTime - DateTimeOffset.UtcNow).TotalMilliseconds; + if (delay > 0) + { + return AddPositiveJitter(Math.Min(delay, MaxRetryDelayMs)); + } + } + } + + var exponentialDelay = Math.Min(BaseRetryDelay * (1 << retryAttempt), MaxRetryDelayMs); + return AddSymmetricJitter(exponentialDelay); + } + + private static bool IsRetryableContent(HttpRequestMessage request) + { + return request.Content switch + { + IIsRetryableContent c => c.IsRetryable, + StreamContent => false, + MultipartContent content => !content.Any(c => c is StreamContent), + _ => true, + }; + } + + internal async global::System.Threading.Tasks.Task CreateHttpRequestAsync( + global::SeedSimpleApi.Core.BaseRequest request + ) + { + var url = BuildUrl(request); + var httpRequest = new HttpRequestMessage(request.Method, url); + httpRequest.Content = request.CreateContent(); + SetHeaders(httpRequest, request.Headers); + + return httpRequest; + } + + private string BuildUrl(global::SeedSimpleApi.Core.BaseRequest request) + { + var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl ?? Options.BaseUrl; + + var trimmedBaseUrl = baseUrl.TrimEnd('/'); + var trimmedBasePath = request.Path.TrimStart('/'); + var url = $"{trimmedBaseUrl}/{trimmedBasePath}"; + + // Append query string if present + if (!string.IsNullOrEmpty(request.QueryString)) + { + return url + request.QueryString; + } + + return url; + } + + private void SetHeaders(HttpRequestMessage httpRequest, Dictionary? headers) + { + if (headers is null) + { + return; + } + + foreach (var kv in headers) + { + if (kv.Value is null) + { + continue; + } + + httpRequest.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + } + + private static (Encoding encoding, string? charset, string mediaType) ParseContentTypeOrDefault( + string? contentType, + Encoding encodingFallback, + string mediaTypeFallback + ) + { + var encoding = encodingFallback; + var mediaType = mediaTypeFallback; + string? charset = null; + if (string.IsNullOrEmpty(contentType)) + { + return (encoding, charset, mediaType); + } + + if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaTypeHeaderValue)) + { + return (encoding, charset, mediaType); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet)) + { + charset = mediaTypeHeaderValue.CharSet; + encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet); + } + + if (!string.IsNullOrEmpty(mediaTypeHeaderValue.MediaType)) + { + mediaType = mediaTypeHeaderValue.MediaType; + } + + return (encoding, charset, mediaType); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawResponse.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawResponse.cs new file mode 100644 index 000000000000..60a857abbb0c --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/RawResponse.cs @@ -0,0 +1,24 @@ +using global::System.Net; + +namespace SeedSimpleApi.Core; + +/// +/// Contains HTTP response metadata including status code, URL, and headers. +/// +public record RawResponse +{ + /// + /// The HTTP status code of the response. + /// + public required HttpStatusCode StatusCode { get; init; } + + /// + /// The request URL that generated this response. + /// + public required Uri Url { get; init; } + + /// + /// The HTTP response headers. + /// + public required Core.ResponseHeaders Headers { get; init; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ResponseHeaders.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ResponseHeaders.cs new file mode 100644 index 000000000000..84e5771de16e --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ResponseHeaders.cs @@ -0,0 +1,108 @@ +using global::System.Collections; +using global::System.Net.Http.Headers; + +namespace SeedSimpleApi.Core; + +/// +/// Represents HTTP response headers with case-insensitive lookup. +/// +public readonly struct ResponseHeaders : IEnumerable +{ + private readonly HttpResponseHeaders? _headers; + private readonly HttpContentHeaders? _contentHeaders; + + private ResponseHeaders(HttpResponseHeaders headers, HttpContentHeaders? contentHeaders) + { + _headers = headers; + _contentHeaders = contentHeaders; + } + + /// + /// Gets the Content-Type header value, if present. + /// + public string? ContentType => _contentHeaders?.ContentType?.ToString(); + + /// + /// Gets the Content-Length header value, if present. + /// + public long? ContentLength => _contentHeaders?.ContentLength; + + /// + /// Creates a ResponseHeaders instance from an HttpResponseMessage. + /// + public static ResponseHeaders FromHttpResponseMessage(HttpResponseMessage response) + { + return new ResponseHeaders(response.Headers, response.Content?.Headers); + } + + /// + /// Tries to get a single header value. Returns the first value if multiple values exist. + /// + public bool TryGetValue(string name, out string? value) + { + if (TryGetValues(name, out var values) && values is not null) + { + value = values.FirstOrDefault(); + return true; + } + + value = null; + return false; + } + + /// + /// Tries to get all values for a header. + /// + public bool TryGetValues(string name, out IEnumerable? values) + { + if (_headers?.TryGetValues(name, out values) == true) + { + return true; + } + + if (_contentHeaders?.TryGetValues(name, out values) == true) + { + return true; + } + + values = null; + return false; + } + + /// + /// Checks if the headers contain a specific header name. + /// + public bool Contains(string name) + { + return _headers?.Contains(name) == true || _contentHeaders?.Contains(name) == true; + } + + /// + /// Gets an enumerator for all headers. + /// + public IEnumerator GetEnumerator() + { + if (_headers is not null) + { + foreach (var header in _headers) + { + yield return new HttpHeader(header.Key, string.Join(", ", header.Value)); + } + } + + if (_contentHeaders is not null) + { + foreach (var header in _contentHeaders) + { + yield return new HttpHeader(header.Key, string.Join(", ", header.Value)); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// +/// Represents a single HTTP header. +/// +public readonly record struct HttpHeader(string Name, string Value); diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StreamRequest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StreamRequest.cs new file mode 100644 index 000000000000..ce7a328feba3 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StreamRequest.cs @@ -0,0 +1,29 @@ +using global::System.Net.Http; +using global::System.Net.Http.Headers; + +namespace SeedSimpleApi.Core; + +/// +/// The request object to be sent for streaming uploads. +/// +internal record StreamRequest : BaseRequest +{ + internal Stream? Body { get; init; } + + internal override HttpContent? CreateContent() + { + if (Body is null) + { + return null; + } + + var content = new StreamContent(Body) + { + Headers = + { + ContentType = MediaTypeHeaderValue.Parse(ContentType ?? "application/octet-stream"), + }, + }; + return content; + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnum.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnum.cs new file mode 100644 index 000000000000..310bdcdc01a2 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnum.cs @@ -0,0 +1,6 @@ +namespace SeedSimpleApi.Core; + +public interface IStringEnum : IEquatable +{ + public string Value { get; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumExtensions.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumExtensions.cs new file mode 100644 index 000000000000..2e8f44638ad8 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumExtensions.cs @@ -0,0 +1,6 @@ +namespace SeedSimpleApi.Core; + +internal static class StringEnumExtensions +{ + public static string Stringify(this IStringEnum stringEnum) => stringEnum.Value; +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs new file mode 100644 index 000000000000..f57d66a0377b --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs @@ -0,0 +1,25 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace SeedSimpleApi.Core; + +internal class StringEnumSerializer : JsonConverter + where T : IStringEnum +{ + public override T? Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception("The JSON value could not be read as a string."); + return (T?)Activator.CreateInstance(typeToConvert, stringValue); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ValueConvert.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ValueConvert.cs new file mode 100644 index 000000000000..9006715aeb6e --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/ValueConvert.cs @@ -0,0 +1,114 @@ +using global::System.Globalization; + +namespace SeedSimpleApi.Core; + +/// +/// Convert values to string for path and query parameters. +/// +public static class ValueConvert +{ + internal static string ToPathParameterString(T value) => ToString(value); + + internal static string ToPathParameterString(bool v) => ToString(v); + + internal static string ToPathParameterString(int v) => ToString(v); + + internal static string ToPathParameterString(long v) => ToString(v); + + internal static string ToPathParameterString(float v) => ToString(v); + + internal static string ToPathParameterString(double v) => ToString(v); + + internal static string ToPathParameterString(decimal v) => ToString(v); + + internal static string ToPathParameterString(short v) => ToString(v); + + internal static string ToPathParameterString(ushort v) => ToString(v); + + internal static string ToPathParameterString(uint v) => ToString(v); + + internal static string ToPathParameterString(ulong v) => ToString(v); + + internal static string ToPathParameterString(string v) => ToString(v); + + internal static string ToPathParameterString(char v) => ToString(v); + + internal static string ToPathParameterString(Guid v) => ToString(v); + + internal static string ToQueryStringValue(T value) => value is null ? "" : ToString(value); + + internal static string ToQueryStringValue(bool v) => ToString(v); + + internal static string ToQueryStringValue(int v) => ToString(v); + + internal static string ToQueryStringValue(long v) => ToString(v); + + internal static string ToQueryStringValue(float v) => ToString(v); + + internal static string ToQueryStringValue(double v) => ToString(v); + + internal static string ToQueryStringValue(decimal v) => ToString(v); + + internal static string ToQueryStringValue(short v) => ToString(v); + + internal static string ToQueryStringValue(ushort v) => ToString(v); + + internal static string ToQueryStringValue(uint v) => ToString(v); + + internal static string ToQueryStringValue(ulong v) => ToString(v); + + internal static string ToQueryStringValue(string v) => v is null ? "" : v; + + internal static string ToQueryStringValue(char v) => ToString(v); + + internal static string ToQueryStringValue(Guid v) => ToString(v); + + internal static string ToString(T value) + { + return value switch + { + null => "null", + string str => str, + true => "true", + false => "false", + int i => ToString(i), + long l => ToString(l), + float f => ToString(f), + double d => ToString(d), + decimal dec => ToString(dec), + short s => ToString(s), + ushort u => ToString(u), + uint u => ToString(u), + ulong u => ToString(u), + char c => ToString(c), + Guid guid => ToString(guid), + _ => JsonUtils.SerializeRelaxedEscaping(value, value.GetType()).Trim('"'), + }; + } + + internal static string ToString(bool v) => v ? "true" : "false"; + + internal static string ToString(int v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(long v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(float v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(double v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(decimal v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(short v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(ushort v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(uint v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(ulong v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(char v) => v.ToString(CultureInfo.InvariantCulture); + + internal static string ToString(string v) => v; + + internal static string ToString(Guid v) => v.ToString("D"); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/ISeedSimpleApiClient.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/ISeedSimpleApiClient.cs new file mode 100644 index 000000000000..68304a504c15 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/ISeedSimpleApiClient.cs @@ -0,0 +1,6 @@ +namespace SeedSimpleApi; + +public partial interface ISeedSimpleApiClient +{ + public IUserClient User { get; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.Custom.props b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.Custom.props new file mode 100644 index 000000000000..17a84cada530 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.Custom.props @@ -0,0 +1,20 @@ + + + + diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.csproj b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.csproj new file mode 100644 index 000000000000..937cda97b23c --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApi.csproj @@ -0,0 +1,58 @@ + + + net462;net8.0;netstandard2.0 + enable + 12 + enable + 0.0.1 + $(Version) + $(Version) + README.md + https://github.com/simple-api/fern + true + + + + false + + + $(DefineConstants);USE_PORTABLE_DATE_ONLY + true + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + <_Parameter1>SeedSimpleApi.Test + + + + + diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs new file mode 100644 index 000000000000..15b170641a08 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/SeedSimpleApiClient.cs @@ -0,0 +1,41 @@ +using SeedSimpleApi.Core; + +namespace SeedSimpleApi; + +public partial class SeedSimpleApiClient : ISeedSimpleApiClient +{ + private readonly RawClient _client; + + public SeedSimpleApiClient(string? token = null, ClientOptions? clientOptions = null) + { + clientOptions ??= new ClientOptions(); + var platformHeaders = new Headers( + new Dictionary() + { + { "X-Fern-Language", "C#" }, + { "X-Fern-SDK-Name", "SeedSimpleApi" }, + { "X-Fern-SDK-Version", Version.Current }, + { "User-Agent", "Fernsimple-api/0.0.1" }, + } + ); + foreach (var header in platformHeaders) + { + if (!clientOptions.Headers.ContainsKey(header.Key)) + { + clientOptions.Headers[header.Key] = header.Value; + } + } + var clientOptionsWithAuth = clientOptions.Clone(); + var authHeaders = new Headers( + new Dictionary() { { "Authorization", $"Bearer {token ?? ""}" } } + ); + foreach (var header in authHeaders) + { + clientOptionsWithAuth.Headers[header.Key] = header.Value; + } + _client = new RawClient(clientOptionsWithAuth); + User = new UserClient(_client); + } + + public IUserClient User { get; } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/IUserClient.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/IUserClient.cs new file mode 100644 index 000000000000..7490bc31b97c --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/IUserClient.cs @@ -0,0 +1,10 @@ +namespace SeedSimpleApi; + +public partial interface IUserClient +{ + WithRawResponseTask GetAsync( + string id, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ); +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/Types/User.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/Types/User.cs new file mode 100644 index 000000000000..242680c05b85 --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/Types/User.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi; + +[Serializable] +public record User : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("id")] + public required string Id { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("email")] + public required string Email { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/UserClient.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/UserClient.cs new file mode 100644 index 000000000000..31b2e5c7d7bd --- /dev/null +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/User/UserClient.cs @@ -0,0 +1,91 @@ +using System.Text.Json; +using SeedSimpleApi.Core; + +namespace SeedSimpleApi; + +public partial class UserClient : IUserClient +{ + private readonly RawClient _client; + + internal UserClient(RawClient client) + { + _client = client; + } + + private async Task> GetAsyncCore( + string id, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + var _headers = await new SeedSimpleApi.Core.HeadersBuilder.Builder() + .Add(_client.Options.Headers) + .Add(_client.Options.AdditionalHeaders) + .Add(options?.AdditionalHeaders) + .BuildAsync() + .ConfigureAwait(false); + var response = await _client + .SendRequestAsync( + new JsonRequest + { + Method = HttpMethod.Get, + Path = string.Format("/users/{0}", ValueConvert.ToPathParameterString(id)), + Headers = _headers, + Options = options, + }, + cancellationToken + ) + .ConfigureAwait(false); + if (response.StatusCode is >= 200 and < 400) + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + try + { + var responseData = JsonUtils.Deserialize(responseBody)!; + return new WithRawResponse() + { + Data = responseData, + RawResponse = new RawResponse() + { + StatusCode = response.Raw.StatusCode, + Url = response.Raw.RequestMessage?.RequestUri ?? new Uri("about:blank"), + Headers = ResponseHeaders.FromHttpResponseMessage(response.Raw), + }, + }; + } + catch (JsonException e) + { + throw new SeedSimpleApiApiException( + "Failed to deserialize response", + response.StatusCode, + responseBody, + e + ); + } + } + { + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + throw new SeedSimpleApiApiException( + $"Error with status code {response.StatusCode}", + response.StatusCode, + responseBody + ); + } + } + + /// + /// await client.User.GetAsync("id"); + /// + public WithRawResponseTask GetAsync( + string id, + RequestOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return new WithRawResponseTask(GetAsyncCore(id, options, cancellationToken)); + } +} From 155f62e3d45ad3d871c921aca67e37b8b5a3daf0 Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:35:43 -0400 Subject: [PATCH 09/29] chore(csharp): update csharp-model seed (#13526) Co-authored-by: fern-support --- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedAccept/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedAlias/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedAnyAuth/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Ast/PrimitiveValue.cs | 29 +++- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Ast/PrimitiveValue.cs | 29 +++- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../src/SeedApi/Dataservice/IndexType.cs | 29 +++- .../src/SeedApi/FieldBehavior.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedErrors/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../examples/src/SeedExamples/BasicType.cs | 29 +++- .../examples/src/SeedExamples/ComplexType.cs | 29 +++- .../SeedExamples/Core/StringEnumSerializer.cs | 25 ---- .../src/SeedExamples/Types/MigrationStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Endpoints/Put/ErrorCategory.cs | 29 +++- .../SeedExhaustive/Endpoints/Put/ErrorCode.cs | 29 +++- .../Types/Enum/WeatherReport.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedExtends/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../src/SeedFileUpload/Service/ObjectType.cs | 29 +++- .../SeedFileUpload/Service/OpenEnumType.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedHttpHead/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Payment/Currency.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedLicense/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedLiteral/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../SeedMixedCase/Service/ResourceStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../src/SeedMultiLineDocs/Operand.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../NullableOptional/UserRole.cs | 29 +++- .../NullableOptional/UserStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedNullable/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedObject/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../SeedObjectsWithImports/File/FileInfo.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../MultipleFilterSearchRequestOperator.cs | 32 +++- .../SingleFilterSearchRequestOperator.cs | 32 +++- .../Core/StringEnumSerializer.cs | 25 ---- .../InlineUsers/InlineUsers/Order.cs | 29 +++- .../src/SeedPagination/Users/Order.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../trace/src/SeedTrace/Commons/Language.cs | 29 +++- .../SeedTrace/Core/StringEnumSerializer.cs | 25 ---- .../SeedTrace/Migration/MigrationStatus.cs | 29 +++- .../SeedTrace/Playlist/ReservedKeywordEnum.cs | 29 +++- .../Submission/ExecutionSessionStatus.cs | 29 +++- .../Submission/RunningSubmissionState.cs | 29 +++- .../Submission/SubmissionTypeEnum.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedUnions/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedUnions/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../validation/src/SeedValidation/Shape.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedVersion/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedVersion/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../src/SeedApi/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../SeedWebhooks/Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/StringEnumSerializer.cs | 25 ---- 251 files changed, 818 insertions(+), 18122 deletions(-) delete mode 100644 seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/alias/src/SeedAlias/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/circular-references/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/errors/src/SeedErrors/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/examples/src/SeedExamples/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/exhaustive/src/SeedExhaustive/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/extends/src/SeedExtends/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/folders/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/imdb/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/license/src/SeedLicense/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/literal/src/SeedLiteral/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/nullable/src/SeedNullable/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/object/src/SeedObject/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/optional/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/pagination/src/SeedPagination/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/request-parameters/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/request-parameters/src/SeedRequestParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/required-nullable/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/required-nullable/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/simple-api/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/simple-api/src/SeedSimpleApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/streaming/src/SeedStreaming/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/trace/src/SeedTrace/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/unions/src/SeedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/validation/src/SeedValidation/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/variables/src/SeedVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/version/src/SeedVersion/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-model/websocket/src/SeedWebsocket/Core/StringEnumSerializer.cs diff --git a/seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 8a644106d764..000000000000 --- a/seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAccept.Core; - -namespace SeedAccept.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs b/seed/csharp-model/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs deleted file mode 100644 index 7e355c53ba43..000000000000 --- a/seed/csharp-model/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAccept.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 5640fc9654b2..000000000000 --- a/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAliasExtends.Core; - -namespace SeedAliasExtends.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs b/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs deleted file mode 100644 index 3f9774619a54..000000000000 --- a/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAliasExtends.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 777a87c0745b..000000000000 --- a/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAlias.Core; - -namespace SeedAlias.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/alias/src/SeedAlias/Core/StringEnumSerializer.cs b/seed/csharp-model/alias/src/SeedAlias/Core/StringEnumSerializer.cs deleted file mode 100644 index c75270e371f2..000000000000 --- a/seed/csharp-model/alias/src/SeedAlias/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAlias.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 5845d3feed96..000000000000 --- a/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAnyAuth.Core; - -namespace SeedAnyAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs b/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 5b1c932d88f7..000000000000 --- a/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAnyAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 741f8bf05408..000000000000 --- a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApiWideBasePath.Core; - -namespace SeedApiWideBasePath.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs deleted file mode 100644 index e2b4f871c0c6..000000000000 --- a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApiWideBasePath.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d8d1102f3f1a..000000000000 --- a/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAudiences.Core; - -namespace SeedAudiences.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs b/seed/csharp-model/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs deleted file mode 100644 index b60613707307..000000000000 --- a/seed/csharp-model/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAudiences.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7f5e3c2b7227..000000000000 --- a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBasicAuthEnvironmentVariables.Core; - -namespace SeedBasicAuthEnvironmentVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index e175e622ebd7..000000000000 --- a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBasicAuthEnvironmentVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 240a5a8748f1..000000000000 --- a/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBasicAuth.Core; - -namespace SeedBasicAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs b/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index bb209f14a8d5..000000000000 --- a/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBasicAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3903298a97a6..000000000000 --- a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBearerTokenEnvironmentVariable.Core; - -namespace SeedBearerTokenEnvironmentVariable.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs deleted file mode 100644 index 843499ec1ed4..000000000000 --- a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBearerTokenEnvironmentVariable.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 967ac07fedbf..000000000000 --- a/seed/csharp-model/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBytesDownload.Core; - -namespace SeedBytesDownload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs b/seed/csharp-model/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs deleted file mode 100644 index 38e325564436..000000000000 --- a/seed/csharp-model/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBytesDownload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 212f8eabdcc3..000000000000 --- a/seed/csharp-model/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBytesUpload.Core; - -namespace SeedBytesUpload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs b/seed/csharp-model/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs deleted file mode 100644 index 18200067ff66..000000000000 --- a/seed/csharp-model/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBytesUpload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/circular-references-advanced/src/SeedApi/Ast/PrimitiveValue.cs b/seed/csharp-model/circular-references-advanced/src/SeedApi/Ast/PrimitiveValue.cs index 04cb020eb5c0..92a7b6f91f86 100644 --- a/seed/csharp-model/circular-references-advanced/src/SeedApi/Ast/PrimitiveValue.cs +++ b/seed/csharp-model/circular-references-advanced/src/SeedApi/Ast/PrimitiveValue.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(PrimitiveValue.PrimitiveValueSerializer))] [Serializable] public readonly record struct PrimitiveValue : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator PrimitiveValue(string value) => new(value); + internal class PrimitiveValueSerializer : JsonConverter + { + public override PrimitiveValue Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new PrimitiveValue(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + PrimitiveValue value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/circular-references/src/SeedApi/Ast/PrimitiveValue.cs b/seed/csharp-model/circular-references/src/SeedApi/Ast/PrimitiveValue.cs index 04cb020eb5c0..92a7b6f91f86 100644 --- a/seed/csharp-model/circular-references/src/SeedApi/Ast/PrimitiveValue.cs +++ b/seed/csharp-model/circular-references/src/SeedApi/Ast/PrimitiveValue.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(PrimitiveValue.PrimitiveValueSerializer))] [Serializable] public readonly record struct PrimitiveValue : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator PrimitiveValue(string value) => new(value); + internal class PrimitiveValueSerializer : JsonConverter + { + public override PrimitiveValue Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new PrimitiveValue(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + PrimitiveValue value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/circular-references/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/circular-references/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/circular-references/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 92795eda1ee0..000000000000 --- a/seed/csharp-model/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedClientSideParams.Core; - -namespace SeedClientSideParams.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs b/seed/csharp-model/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs deleted file mode 100644 index 644105e8917a..000000000000 --- a/seed/csharp-model/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedClientSideParams.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b4e40dca3030..000000000000 --- a/seed/csharp-model/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedContentTypes.Core; - -namespace SeedContentTypes.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs b/seed/csharp-model/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs deleted file mode 100644 index 3fd0bb635906..000000000000 --- a/seed/csharp-model/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedContentTypes.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bacc5e8795ff..000000000000 --- a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCrossPackageTypeNames.Core; - -namespace SeedCrossPackageTypeNames.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs deleted file mode 100644 index ef28adebef97..000000000000 --- a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCrossPackageTypeNames.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs index efdca475318f..05b6fe6d5ff9 100644 --- a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Dataservice/IndexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(IndexType.IndexTypeSerializer))] [Serializable] public readonly record struct IndexType : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator IndexType(string value) => new(value); + internal class IndexTypeSerializer : JsonConverter + { + public override IndexType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new IndexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + IndexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs index 38cc250adbcf..a291319f57db 100644 --- a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/FieldBehavior.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(FieldBehavior.FieldBehaviorSerializer))] [Serializable] public readonly record struct FieldBehavior : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator FieldBehavior(string value) => new(value); + internal class FieldBehaviorSerializer : JsonConverter + { + public override FieldBehavior Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new FieldBehavior(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + FieldBehavior value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 06b755e1e3f0..000000000000 --- a/seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpNamespaceCollision.Core; - -namespace SeedCsharpNamespaceCollision.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs b/seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs deleted file mode 100644 index a243d2788f31..000000000000 --- a/seed/csharp-model/csharp-namespace-collision/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpNamespaceCollision.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index ce48e4ca8e55..000000000000 --- a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpNamespaceConflict.Core; - -namespace SeedCsharpNamespaceConflict.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs deleted file mode 100644 index f04938b2225c..000000000000 --- a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpNamespaceConflict.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 80a0d8d5cacb..000000000000 --- a/seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpReadonlyRequest.Core; - -namespace SeedCsharpReadonlyRequest.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs b/seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs deleted file mode 100644 index 0184add58312..000000000000 --- a/seed/csharp-model/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpReadonlyRequest.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 551aec342853..000000000000 --- a/seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpSystemCollision.Core; - -namespace SeedCsharpSystemCollision.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs b/seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs deleted file mode 100644 index b0ed5550cfa4..000000000000 --- a/seed/csharp-model/csharp-system-collision/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpSystemCollision.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dba8b129db8f..000000000000 --- a/seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpXmlEntities.Core; - -namespace SeedCsharpXmlEntities.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs b/seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs deleted file mode 100644 index 10b2e9349ab8..000000000000 --- a/seed/csharp-model/csharp-xml-entities/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpXmlEntities.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 5fa48d30deff..000000000000 --- a/seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedDollarStringExamples.Core; - -namespace SeedDollarStringExamples.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs b/seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs deleted file mode 100644 index 6f3607e5000f..000000000000 --- a/seed/csharp-model/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedDollarStringExamples.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d94e172d938b..000000000000 --- a/seed/csharp-model/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEmptyClients.Core; - -namespace SeedEmptyClients.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs b/seed/csharp-model/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs deleted file mode 100644 index bc9e216c4731..000000000000 --- a/seed/csharp-model/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEmptyClients.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index cc6bec4aa072..000000000000 --- a/seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEndpointSecurityAuth.Core; - -namespace SeedEndpointSecurityAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs b/seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 6227557247b4..000000000000 --- a/seed/csharp-model/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEndpointSecurityAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index df9da3401d28..000000000000 --- a/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedErrorProperty.Core; - -namespace SeedErrorProperty.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs b/seed/csharp-model/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs deleted file mode 100644 index 0957a02c99d2..000000000000 --- a/seed/csharp-model/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedErrorProperty.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 94f333d30865..000000000000 --- a/seed/csharp-model/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedErrors.Core; - -namespace SeedErrors.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/errors/src/SeedErrors/Core/StringEnumSerializer.cs b/seed/csharp-model/errors/src/SeedErrors/Core/StringEnumSerializer.cs deleted file mode 100644 index 02a5777c4dca..000000000000 --- a/seed/csharp-model/errors/src/SeedErrors/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedErrors.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f43b68408195..000000000000 --- a/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExamples.Core; - -namespace SeedExamples.Test_.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/examples/src/SeedExamples/BasicType.cs b/seed/csharp-model/examples/src/SeedExamples/BasicType.cs index f913ec1dadcc..f061512fe2bd 100644 --- a/seed/csharp-model/examples/src/SeedExamples/BasicType.cs +++ b/seed/csharp-model/examples/src/SeedExamples/BasicType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(BasicType.BasicTypeSerializer))] [Serializable] public readonly record struct BasicType : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator BasicType(string value) => new(value); + internal class BasicTypeSerializer : JsonConverter + { + public override BasicType Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new BasicType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + BasicType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/examples/src/SeedExamples/ComplexType.cs b/seed/csharp-model/examples/src/SeedExamples/ComplexType.cs index 8516776b91dc..7fdeabb1f778 100644 --- a/seed/csharp-model/examples/src/SeedExamples/ComplexType.cs +++ b/seed/csharp-model/examples/src/SeedExamples/ComplexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ComplexType.ComplexTypeSerializer))] [Serializable] public readonly record struct ComplexType : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ComplexType(string value) => new(value); + internal class ComplexTypeSerializer : JsonConverter + { + public override ComplexType Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ComplexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ComplexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/examples/src/SeedExamples/Core/StringEnumSerializer.cs b/seed/csharp-model/examples/src/SeedExamples/Core/StringEnumSerializer.cs deleted file mode 100644 index 7c04fcb7cf4e..000000000000 --- a/seed/csharp-model/examples/src/SeedExamples/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExamples.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/examples/src/SeedExamples/Types/MigrationStatus.cs b/seed/csharp-model/examples/src/SeedExamples/Types/MigrationStatus.cs index 0656831be6bb..69739723cfe9 100644 --- a/seed/csharp-model/examples/src/SeedExamples/Types/MigrationStatus.cs +++ b/seed/csharp-model/examples/src/SeedExamples/Types/MigrationStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(MigrationStatus.MigrationStatusSerializer))] [Serializable] public readonly record struct MigrationStatus : IStringEnum { @@ -60,6 +61,32 @@ public override string ToString() public static explicit operator MigrationStatus(string value) => new(value); + internal class MigrationStatusSerializer : JsonConverter + { + public override MigrationStatus Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MigrationStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MigrationStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dfe652126532..000000000000 --- a/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExhaustive.Core; - -namespace SeedExhaustive.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/StringEnumSerializer.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/StringEnumSerializer.cs deleted file mode 100644 index a6c0d9acc65b..000000000000 --- a/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExhaustive.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCategory.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCategory.cs index f2345ce44866..c9c55248949b 100644 --- a/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCategory.cs +++ b/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCategory.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCategory.ErrorCategorySerializer))] [Serializable] public readonly record struct ErrorCategory : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ErrorCategory(string value) => new(value); + internal class ErrorCategorySerializer : JsonConverter + { + public override ErrorCategory Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCategory(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCategory value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCode.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCode.cs index 4daf8d0a2a36..66403c0f64de 100644 --- a/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCode.cs +++ b/seed/csharp-model/exhaustive/src/SeedExhaustive/Endpoints/Put/ErrorCode.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Endpoints; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ErrorCode.ErrorCodeSerializer))] [Serializable] public readonly record struct ErrorCode : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator ErrorCode(string value) => new(value); + internal class ErrorCodeSerializer : JsonConverter + { + public override ErrorCode Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ErrorCode(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorCode value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive/Types/Enum/WeatherReport.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive/Types/Enum/WeatherReport.cs index a9a3f5183f39..bf9ef27df567 100644 --- a/seed/csharp-model/exhaustive/src/SeedExhaustive/Types/Enum/WeatherReport.cs +++ b/seed/csharp-model/exhaustive/src/SeedExhaustive/Types/Enum/WeatherReport.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExhaustive.Core; namespace SeedExhaustive.Types; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WeatherReport.WeatherReportSerializer))] [Serializable] public readonly record struct WeatherReport : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator WeatherReport(string value) => new(value); + internal class WeatherReportSerializer : JsonConverter + { + public override WeatherReport Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WeatherReport(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WeatherReport value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index c2ca469ffefc..000000000000 --- a/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExtends.Core; - -namespace SeedExtends.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/extends/src/SeedExtends/Core/StringEnumSerializer.cs b/seed/csharp-model/extends/src/SeedExtends/Core/StringEnumSerializer.cs deleted file mode 100644 index fc76c249fb2c..000000000000 --- a/seed/csharp-model/extends/src/SeedExtends/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExtends.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e9dc842d9c71..000000000000 --- a/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExtraProperties.Core; - -namespace SeedExtraProperties.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs b/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs deleted file mode 100644 index 0896fa563630..000000000000 --- a/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExtraProperties.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 33a2be57c538..000000000000 --- a/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedFileDownload.Core; - -namespace SeedFileDownload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs b/seed/csharp-model/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs deleted file mode 100644 index d0995cbaacaf..000000000000 --- a/seed/csharp-model/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedFileDownload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 1e40f9937f4a..000000000000 --- a/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedFileUpload.Core; - -namespace SeedFileUpload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs b/seed/csharp-model/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs deleted file mode 100644 index 64a023082141..000000000000 --- a/seed/csharp-model/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedFileUpload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload/Service/ObjectType.cs b/seed/csharp-model/file-upload/src/SeedFileUpload/Service/ObjectType.cs index e9124b25ec94..a4e2ff643b7d 100644 --- a/seed/csharp-model/file-upload/src/SeedFileUpload/Service/ObjectType.cs +++ b/seed/csharp-model/file-upload/src/SeedFileUpload/Service/ObjectType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedFileUpload.Core; namespace SeedFileUpload; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ObjectType.ObjectTypeSerializer))] [Serializable] public readonly record struct ObjectType : IStringEnum { @@ -51,6 +52,32 @@ public override string ToString() public static explicit operator ObjectType(string value) => new(value); + internal class ObjectTypeSerializer : JsonConverter + { + public override ObjectType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ObjectType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ObjectType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload/Service/OpenEnumType.cs b/seed/csharp-model/file-upload/src/SeedFileUpload/Service/OpenEnumType.cs index 18257d195d2a..97e49152794d 100644 --- a/seed/csharp-model/file-upload/src/SeedFileUpload/Service/OpenEnumType.cs +++ b/seed/csharp-model/file-upload/src/SeedFileUpload/Service/OpenEnumType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedFileUpload.Core; namespace SeedFileUpload; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(OpenEnumType.OpenEnumTypeSerializer))] [Serializable] public readonly record struct OpenEnumType : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator OpenEnumType(string value) => new(value); + internal class OpenEnumTypeSerializer : JsonConverter + { + public override OpenEnumType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new OpenEnumType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + OpenEnumType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/folders/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/folders/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/folders/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 9af5ccf52cba..000000000000 --- a/seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedHeaderTokenEnvironmentVariable.Core; - -namespace SeedHeaderTokenEnvironmentVariable.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs b/seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs deleted file mode 100644 index 0488a58c6d46..000000000000 --- a/seed/csharp-model/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedHeaderTokenEnvironmentVariable.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 21eeab0ef3fa..000000000000 --- a/seed/csharp-model/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedHeaderToken.Core; - -namespace SeedHeaderToken.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs b/seed/csharp-model/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs deleted file mode 100644 index 9e227b9d6f60..000000000000 --- a/seed/csharp-model/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedHeaderToken.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 95f0c75902a2..000000000000 --- a/seed/csharp-model/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedHttpHead.Core; - -namespace SeedHttpHead.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs b/seed/csharp-model/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs deleted file mode 100644 index 82d2fe43da3b..000000000000 --- a/seed/csharp-model/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedHttpHead.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index ac56a14a1709..000000000000 --- a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedIdempotencyHeaders.Core; - -namespace SeedIdempotencyHeaders.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs deleted file mode 100644 index 35ba57b65c17..000000000000 --- a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedIdempotencyHeaders.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Currency.cs b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Currency.cs index ae848aa7a451..56ca6f19e1c9 100644 --- a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Currency.cs +++ b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Currency.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedIdempotencyHeaders.Core; namespace SeedIdempotencyHeaders; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Currency.CurrencySerializer))] [Serializable] public readonly record struct Currency : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Currency(string value) => new(value); + internal class CurrencySerializer : JsonConverter + { + public override Currency Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Currency(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Currency value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/imdb/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/imdb/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/imdb/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 2a06c7fe6c39..000000000000 --- a/seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthExplicit.Core; - -namespace SeedInferredAuthExplicit.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs b/seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs deleted file mode 100644 index 2796a6d0f563..000000000000 --- a/seed/csharp-model/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthExplicit.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7298734f93bc..000000000000 --- a/seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicitApiKey.Core; - -namespace SeedInferredAuthImplicitApiKey.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs b/seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs deleted file mode 100644 index 2ee535db7e62..000000000000 --- a/seed/csharp-model/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicitApiKey.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 6c66465228af..000000000000 --- a/seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicitNoExpiry.Core; - -namespace SeedInferredAuthImplicitNoExpiry.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs b/seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs deleted file mode 100644 index bc96fa39117c..000000000000 --- a/seed/csharp-model/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicitNoExpiry.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 728bd7d4e345..000000000000 --- a/seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicit.Core; - -namespace SeedInferredAuthImplicit.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs b/seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs deleted file mode 100644 index 037819cd147e..000000000000 --- a/seed/csharp-model/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicit.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 728bd7d4e345..000000000000 --- a/seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicit.Core; - -namespace SeedInferredAuthImplicit.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs b/seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs deleted file mode 100644 index 037819cd147e..000000000000 --- a/seed/csharp-model/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicit.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7dbd34759269..000000000000 --- a/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLicense.Core; - -namespace SeedLicense.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/license/src/SeedLicense/Core/StringEnumSerializer.cs b/seed/csharp-model/license/src/SeedLicense/Core/StringEnumSerializer.cs deleted file mode 100644 index 699c6fc6a81b..000000000000 --- a/seed/csharp-model/license/src/SeedLicense/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLicense.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a7260ecb8573..000000000000 --- a/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLiteral.Core; - -namespace SeedLiteral.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/literal/src/SeedLiteral/Core/StringEnumSerializer.cs b/seed/csharp-model/literal/src/SeedLiteral/Core/StringEnumSerializer.cs deleted file mode 100644 index e8edea07eba0..000000000000 --- a/seed/csharp-model/literal/src/SeedLiteral/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLiteral.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b37d3c428eee..000000000000 --- a/seed/csharp-model/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLiteralsUnions.Core; - -namespace SeedLiteralsUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs b/seed/csharp-model/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index 3f2a5750b025..000000000000 --- a/seed/csharp-model/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLiteralsUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index abeb13903035..000000000000 --- a/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMixedCase.Core; - -namespace SeedMixedCase.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs b/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs deleted file mode 100644 index e84ac03a9290..000000000000 --- a/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMixedCase.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/mixed-case/src/SeedMixedCase/Service/ResourceStatus.cs b/seed/csharp-model/mixed-case/src/SeedMixedCase/Service/ResourceStatus.cs index ec045d1864b3..3f0a60a5ad55 100644 --- a/seed/csharp-model/mixed-case/src/SeedMixedCase/Service/ResourceStatus.cs +++ b/seed/csharp-model/mixed-case/src/SeedMixedCase/Service/ResourceStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedMixedCase.Core; namespace SeedMixedCase; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ResourceStatus.ResourceStatusSerializer))] [Serializable] public readonly record struct ResourceStatus : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator ResourceStatus(string value) => new(value); + internal class ResourceStatusSerializer : JsonConverter + { + public override ResourceStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ResourceStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ResourceStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 50af30e8c157..000000000000 --- a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMixedFileDirectory.Core; - -namespace SeedMixedFileDirectory.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs deleted file mode 100644 index 118618d004d1..000000000000 --- a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMixedFileDirectory.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 21811a312708..000000000000 --- a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMultiLineDocs.Core; - -namespace SeedMultiLineDocs.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs deleted file mode 100644 index d74ce734bbd7..000000000000 --- a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMultiLineDocs.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Operand.cs b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Operand.cs index 71940b31d01a..cfaed4e2be43 100644 --- a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Operand.cs +++ b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Operand.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedMultiLineDocs.Core; namespace SeedMultiLineDocs; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Operand.OperandSerializer))] [Serializable] public readonly record struct Operand : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator Operand(string value) => new(value); + internal class OperandSerializer : JsonConverter + { + public override Operand Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Operand(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Operand value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index ad80a44bb350..000000000000 --- a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMultiUrlEnvironmentNoDefault.Core; - -namespace SeedMultiUrlEnvironmentNoDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index 8686ac66af1d..000000000000 --- a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMultiUrlEnvironmentNoDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 52e7708a1622..000000000000 --- a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMultiUrlEnvironment.Core; - -namespace SeedMultiUrlEnvironment.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs deleted file mode 100644 index ceba4f5f12da..000000000000 --- a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMultiUrlEnvironment.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index c72b8b5763c3..000000000000 --- a/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNoEnvironment.Core; - -namespace SeedNoEnvironment.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs b/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs deleted file mode 100644 index 58e53ca58c40..000000000000 --- a/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNoEnvironment.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b4bd49f112f8..000000000000 --- a/seed/csharp-model/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNoRetries.Core; - -namespace SeedNoRetries.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs b/seed/csharp-model/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs deleted file mode 100644 index 55b2ff5194a7..000000000000 --- a/seed/csharp-model/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNoRetries.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 31ea594b7ce9..000000000000 --- a/seed/csharp-model/nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNullableOptional.Core; - -namespace SeedNullableOptional.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs b/seed/csharp-model/nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs deleted file mode 100644 index 7cf90a707479..000000000000 --- a/seed/csharp-model/nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNullableOptional.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserRole.cs b/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserRole.cs index b27b033ad94c..3d3a87b97b87 100644 --- a/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserRole.cs +++ b/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserRole.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedNullableOptional.Core; namespace SeedNullableOptional; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(UserRole.UserRoleSerializer))] [Serializable] public readonly record struct UserRole : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator UserRole(string value) => new(value); + internal class UserRoleSerializer : JsonConverter + { + public override UserRole Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new UserRole(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + UserRole value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserStatus.cs b/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserStatus.cs index 08816dfbe443..961e2fd46de4 100644 --- a/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserStatus.cs +++ b/seed/csharp-model/nullable-optional/src/SeedNullableOptional/NullableOptional/UserStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedNullableOptional.Core; namespace SeedNullableOptional; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(UserStatus.UserStatusSerializer))] [Serializable] public readonly record struct UserStatus : IStringEnum { @@ -55,6 +56,32 @@ public override string ToString() public static explicit operator UserStatus(string value) => new(value); + internal class UserStatusSerializer : JsonConverter + { + public override UserStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new UserStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + UserStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 45f53ec6a2b4..000000000000 --- a/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNullable.Core; - -namespace SeedNullable.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/nullable/src/SeedNullable/Core/StringEnumSerializer.cs b/seed/csharp-model/nullable/src/SeedNullable/Core/StringEnumSerializer.cs deleted file mode 100644 index 4a43207ef74b..000000000000 --- a/seed/csharp-model/nullable/src/SeedNullable/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNullable.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index fe3cda738337..000000000000 --- a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentials.Core; - -namespace SeedOauthClientCredentials.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs deleted file mode 100644 index 9c9beac155d8..000000000000 --- a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentials.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 818faaadfd8f..000000000000 --- a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsDefault.Core; - -namespace SeedOauthClientCredentialsDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index 93f05ba87bfd..000000000000 --- a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3d2d134917ff..000000000000 --- a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsEnvironmentVariables.Core; - -namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index 42b2f618f25b..000000000000 --- a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsEnvironmentVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0ddb4c5f73e6..000000000000 --- a/seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsMandatoryAuth.Core; - -namespace SeedOauthClientCredentialsMandatoryAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 107a227b0958..000000000000 --- a/seed/csharp-model/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsMandatoryAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index fe3cda738337..000000000000 --- a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentials.Core; - -namespace SeedOauthClientCredentials.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs deleted file mode 100644 index 9c9beac155d8..000000000000 --- a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentials.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0b921acae0c1..000000000000 --- a/seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsReference.Core; - -namespace SeedOauthClientCredentialsReference.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs deleted file mode 100644 index 4cb5f9473e9f..000000000000 --- a/seed/csharp-model/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsReference.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 223047839c5e..000000000000 --- a/seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsWithVariables.Core; - -namespace SeedOauthClientCredentialsWithVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index 6578e7b20d19..000000000000 --- a/seed/csharp-model/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsWithVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index fe3cda738337..000000000000 --- a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentials.Core; - -namespace SeedOauthClientCredentials.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs deleted file mode 100644 index 9c9beac155d8..000000000000 --- a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentials.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f55a7fff3c15..000000000000 --- a/seed/csharp-model/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedObject.Core; - -namespace SeedObject.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/object/src/SeedObject/Core/StringEnumSerializer.cs b/seed/csharp-model/object/src/SeedObject/Core/StringEnumSerializer.cs deleted file mode 100644 index 777256427c68..000000000000 --- a/seed/csharp-model/object/src/SeedObject/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedObject.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0177a91bd780..000000000000 --- a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedObjectsWithImports.Core; - -namespace SeedObjectsWithImports.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs deleted file mode 100644 index 61b8664dc2b6..000000000000 --- a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedObjectsWithImports.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/File/FileInfo.cs b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/File/FileInfo.cs index c1b256342192..c332b86cf17f 100644 --- a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/File/FileInfo.cs +++ b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/File/FileInfo.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedObjectsWithImports.Core; namespace SeedObjectsWithImports; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(FileInfo.FileInfoSerializer))] [Serializable] public readonly record struct FileInfo : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator FileInfo(string value) => new(value); + internal class FileInfoSerializer : JsonConverter + { + public override FileInfo Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new FileInfo(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + FileInfo value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0177a91bd780..000000000000 --- a/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedObjectsWithImports.Core; - -namespace SeedObjectsWithImports.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs b/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs deleted file mode 100644 index 61b8664dc2b6..000000000000 --- a/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedObjectsWithImports.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bfbae5d43141..000000000000 --- a/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPackageYml.Core; - -namespace SeedPackageYml.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs b/seed/csharp-model/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs deleted file mode 100644 index 3e94be4ad674..000000000000 --- a/seed/csharp-model/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPackageYml.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3629e23bc177..000000000000 --- a/seed/csharp-model/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPagination.Core; - -namespace SeedPagination.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs b/seed/csharp-model/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs deleted file mode 100644 index 7a70d0820d6c..000000000000 --- a/seed/csharp-model/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPagination.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 921247980380..000000000000 --- a/seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPaginationUriPath.Core; - -namespace SeedPaginationUriPath.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath/Core/StringEnumSerializer.cs b/seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath/Core/StringEnumSerializer.cs deleted file mode 100644 index 629f087ef08e..000000000000 --- a/seed/csharp-model/pagination-uri-path/src/SeedPaginationUriPath/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPaginationUriPath.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3629e23bc177..000000000000 --- a/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPagination.Core; - -namespace SeedPagination.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/pagination/src/SeedPagination/Complex/MultipleFilterSearchRequestOperator.cs b/seed/csharp-model/pagination/src/SeedPagination/Complex/MultipleFilterSearchRequestOperator.cs index b1091d0a6cd4..e54c858342f0 100644 --- a/seed/csharp-model/pagination/src/SeedPagination/Complex/MultipleFilterSearchRequestOperator.cs +++ b/seed/csharp-model/pagination/src/SeedPagination/Complex/MultipleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(MultipleFilterSearchRequestOperator.MultipleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct MultipleFilterSearchRequestOperator : IStringEnum { @@ -53,6 +56,33 @@ public static explicit operator string(MultipleFilterSearchRequestOperator value public static explicit operator MultipleFilterSearchRequestOperator(string value) => new(value); + internal class MultipleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override MultipleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MultipleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MultipleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/pagination/src/SeedPagination/Complex/SingleFilterSearchRequestOperator.cs b/seed/csharp-model/pagination/src/SeedPagination/Complex/SingleFilterSearchRequestOperator.cs index 01db9510ded4..77fe8ac1b125 100644 --- a/seed/csharp-model/pagination/src/SeedPagination/Complex/SingleFilterSearchRequestOperator.cs +++ b/seed/csharp-model/pagination/src/SeedPagination/Complex/SingleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(SingleFilterSearchRequestOperator.SingleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct SingleFilterSearchRequestOperator : IStringEnum { @@ -70,6 +73,33 @@ public override string ToString() public static explicit operator SingleFilterSearchRequestOperator(string value) => new(value); + internal class SingleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override SingleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SingleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SingleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/pagination/src/SeedPagination/Core/StringEnumSerializer.cs b/seed/csharp-model/pagination/src/SeedPagination/Core/StringEnumSerializer.cs deleted file mode 100644 index 7a70d0820d6c..000000000000 --- a/seed/csharp-model/pagination/src/SeedPagination/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPagination.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/pagination/src/SeedPagination/InlineUsers/InlineUsers/Order.cs b/seed/csharp-model/pagination/src/SeedPagination/InlineUsers/InlineUsers/Order.cs index 9796bd07808f..c7d36b88358c 100644 --- a/seed/csharp-model/pagination/src/SeedPagination/InlineUsers/InlineUsers/Order.cs +++ b/seed/csharp-model/pagination/src/SeedPagination/InlineUsers/InlineUsers/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination.InlineUsers; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/pagination/src/SeedPagination/Users/Order.cs b/seed/csharp-model/pagination/src/SeedPagination/Users/Order.cs index aef9e7e6d2f4..99fa969b39b5 100644 --- a/seed/csharp-model/pagination/src/SeedPagination/Users/Order.cs +++ b/seed/csharp-model/pagination/src/SeedPagination/Users/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 64281f08642e..000000000000 --- a/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPathParameters.Core; - -namespace SeedPathParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs b/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 328c91741063..000000000000 --- a/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPathParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e392e66771df..000000000000 --- a/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPlainText.Core; - -namespace SeedPlainText.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs b/seed/csharp-model/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs deleted file mode 100644 index ca9f1cd9720d..000000000000 --- a/seed/csharp-model/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPlainText.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 606da7723116..000000000000 --- a/seed/csharp-model/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPropertyAccess.Core; - -namespace SeedPropertyAccess.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs b/seed/csharp-model/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs deleted file mode 100644 index c200fc0b897a..000000000000 --- a/seed/csharp-model/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPropertyAccess.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 84db87ed99f3..000000000000 --- a/seed/csharp-model/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPublicObject.Core; - -namespace SeedPublicObject.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs b/seed/csharp-model/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs deleted file mode 100644 index 730ebe71fdd2..000000000000 --- a/seed/csharp-model/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPublicObject.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 9c05edc6a1db..000000000000 --- a/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedQueryParameters.Core; - -namespace SeedQueryParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs b/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 557e063b3e06..000000000000 --- a/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedQueryParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/request-parameters/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/request-parameters/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 672c248062c6..000000000000 --- a/seed/csharp-model/request-parameters/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedRequestParameters.Core; - -namespace SeedRequestParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/request-parameters/src/SeedRequestParameters/Core/StringEnumSerializer.cs b/seed/csharp-model/request-parameters/src/SeedRequestParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 880a057748c0..000000000000 --- a/seed/csharp-model/request-parameters/src/SeedRequestParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedRequestParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/required-nullable/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/required-nullable/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/required-nullable/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/required-nullable/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/required-nullable/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/required-nullable/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 354f6d7643f5..000000000000 --- a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNurseryApi.Core; - -namespace SeedNurseryApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs deleted file mode 100644 index 61bafd65edb2..000000000000 --- a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNurseryApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 06d735649ff5..000000000000 --- a/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedResponseProperty.Core; - -namespace SeedResponseProperty.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs b/seed/csharp-model/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs deleted file mode 100644 index 1bcb4e6909fd..000000000000 --- a/seed/csharp-model/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedResponseProperty.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bb88c0b06de0..000000000000 --- a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedServerSentEvents.Core; - -namespace SeedServerSentEvents.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs deleted file mode 100644 index a9dba578325f..000000000000 --- a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedServerSentEvents.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bb88c0b06de0..000000000000 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedServerSentEvents.Core; - -namespace SeedServerSentEvents.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs deleted file mode 100644 index a9dba578325f..000000000000 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedServerSentEvents.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/simple-api/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/simple-api/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f79f10bf40eb..000000000000 --- a/seed/csharp-model/simple-api/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSimpleApi.Core; - -namespace SeedSimpleApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/simple-api/src/SeedSimpleApi/Core/StringEnumSerializer.cs b/seed/csharp-model/simple-api/src/SeedSimpleApi/Core/StringEnumSerializer.cs deleted file mode 100644 index f57d66a0377b..000000000000 --- a/seed/csharp-model/simple-api/src/SeedSimpleApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSimpleApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 9690b2c8924b..000000000000 --- a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSingleUrlEnvironmentDefault.Core; - -namespace SeedSingleUrlEnvironmentDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index ab41210fc225..000000000000 --- a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSingleUrlEnvironmentDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 009dce166f4d..000000000000 --- a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSingleUrlEnvironmentNoDefault.Core; - -namespace SeedSingleUrlEnvironmentNoDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index f98cc99fd878..000000000000 --- a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSingleUrlEnvironmentNoDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7301e5bb13d6..000000000000 --- a/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedStreaming.Core; - -namespace SeedStreaming.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs b/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs deleted file mode 100644 index a70911174a2f..000000000000 --- a/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedStreaming.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7301e5bb13d6..000000000000 --- a/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedStreaming.Core; - -namespace SeedStreaming.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/streaming/src/SeedStreaming/Core/StringEnumSerializer.cs b/seed/csharp-model/streaming/src/SeedStreaming/Core/StringEnumSerializer.cs deleted file mode 100644 index a70911174a2f..000000000000 --- a/seed/csharp-model/streaming/src/SeedStreaming/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedStreaming.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a88e4c723a7b..000000000000 --- a/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedTrace.Core; - -namespace SeedTrace.Test_.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/trace/src/SeedTrace/Commons/Language.cs b/seed/csharp-model/trace/src/SeedTrace/Commons/Language.cs index 1a4e792dfea8..1eb0bc178c93 100644 --- a/seed/csharp-model/trace/src/SeedTrace/Commons/Language.cs +++ b/seed/csharp-model/trace/src/SeedTrace/Commons/Language.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Language.LanguageSerializer))] [Serializable] public readonly record struct Language : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator Language(string value) => new(value); + internal class LanguageSerializer : JsonConverter + { + public override Language Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Language(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Language value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/trace/src/SeedTrace/Core/StringEnumSerializer.cs b/seed/csharp-model/trace/src/SeedTrace/Core/StringEnumSerializer.cs deleted file mode 100644 index 9a93ba91f759..000000000000 --- a/seed/csharp-model/trace/src/SeedTrace/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedTrace.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/trace/src/SeedTrace/Migration/MigrationStatus.cs b/seed/csharp-model/trace/src/SeedTrace/Migration/MigrationStatus.cs index d227b996d285..1f8b41f486d4 100644 --- a/seed/csharp-model/trace/src/SeedTrace/Migration/MigrationStatus.cs +++ b/seed/csharp-model/trace/src/SeedTrace/Migration/MigrationStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(MigrationStatus.MigrationStatusSerializer))] [Serializable] public readonly record struct MigrationStatus : IStringEnum { @@ -60,6 +61,32 @@ public override string ToString() public static explicit operator MigrationStatus(string value) => new(value); + internal class MigrationStatusSerializer : JsonConverter + { + public override MigrationStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MigrationStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MigrationStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/trace/src/SeedTrace/Playlist/ReservedKeywordEnum.cs b/seed/csharp-model/trace/src/SeedTrace/Playlist/ReservedKeywordEnum.cs index bf8451443d48..436afb31a207 100644 --- a/seed/csharp-model/trace/src/SeedTrace/Playlist/ReservedKeywordEnum.cs +++ b/seed/csharp-model/trace/src/SeedTrace/Playlist/ReservedKeywordEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReservedKeywordEnum.ReservedKeywordEnumSerializer))] [Serializable] public readonly record struct ReservedKeywordEnum : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator ReservedKeywordEnum(string value) => new(value); + internal class ReservedKeywordEnumSerializer : JsonConverter + { + public override ReservedKeywordEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReservedKeywordEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReservedKeywordEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/trace/src/SeedTrace/Submission/ExecutionSessionStatus.cs b/seed/csharp-model/trace/src/SeedTrace/Submission/ExecutionSessionStatus.cs index 0dc7eadf4116..2f713b6b4c90 100644 --- a/seed/csharp-model/trace/src/SeedTrace/Submission/ExecutionSessionStatus.cs +++ b/seed/csharp-model/trace/src/SeedTrace/Submission/ExecutionSessionStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ExecutionSessionStatus.ExecutionSessionStatusSerializer))] [Serializable] public readonly record struct ExecutionSessionStatus : IStringEnum { @@ -62,6 +63,32 @@ public override string ToString() public static explicit operator ExecutionSessionStatus(string value) => new(value); + internal class ExecutionSessionStatusSerializer : JsonConverter + { + public override ExecutionSessionStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ExecutionSessionStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ExecutionSessionStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/trace/src/SeedTrace/Submission/RunningSubmissionState.cs b/seed/csharp-model/trace/src/SeedTrace/Submission/RunningSubmissionState.cs index 208ca9dfbdf4..2ef80355165d 100644 --- a/seed/csharp-model/trace/src/SeedTrace/Submission/RunningSubmissionState.cs +++ b/seed/csharp-model/trace/src/SeedTrace/Submission/RunningSubmissionState.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(RunningSubmissionState.RunningSubmissionStateSerializer))] [Serializable] public readonly record struct RunningSubmissionState : IStringEnum { @@ -66,6 +67,32 @@ public override string ToString() public static explicit operator RunningSubmissionState(string value) => new(value); + internal class RunningSubmissionStateSerializer : JsonConverter + { + public override RunningSubmissionState Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new RunningSubmissionState(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + RunningSubmissionState value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/trace/src/SeedTrace/Submission/SubmissionTypeEnum.cs b/seed/csharp-model/trace/src/SeedTrace/Submission/SubmissionTypeEnum.cs index 8fedafdbc8f3..34e2abf88591 100644 --- a/seed/csharp-model/trace/src/SeedTrace/Submission/SubmissionTypeEnum.cs +++ b/seed/csharp-model/trace/src/SeedTrace/Submission/SubmissionTypeEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(SubmissionTypeEnum.SubmissionTypeEnumSerializer))] [Serializable] public readonly record struct SubmissionTypeEnum : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator SubmissionTypeEnum(string value) => new(value); + internal class SubmissionTypeEnumSerializer : JsonConverter + { + public override SubmissionTypeEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SubmissionTypeEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SubmissionTypeEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 93cfc064b533..000000000000 --- a/seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUndiscriminatedUnionWithResponseProperty.Core; - -namespace SeedUndiscriminatedUnionWithResponseProperty.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs b/seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs deleted file mode 100644 index 382b481d7a0e..000000000000 --- a/seed/csharp-model/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUndiscriminatedUnionWithResponseProperty.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f820460467fe..000000000000 --- a/seed/csharp-model/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUnions.Core; - -namespace SeedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs b/seed/csharp-model/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index a0bfe7062381..000000000000 --- a/seed/csharp-model/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f820460467fe..000000000000 --- a/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUnions.Core; - -namespace SeedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/unions/src/SeedUnions/Core/StringEnumSerializer.cs b/seed/csharp-model/unions/src/SeedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index a0bfe7062381..000000000000 --- a/seed/csharp-model/unions/src/SeedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f768e44645d3..000000000000 --- a/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUnknownAsAny.Core; - -namespace SeedUnknownAsAny.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs b/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs deleted file mode 100644 index b76f029c8b43..000000000000 --- a/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUnknownAsAny.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index c8ed6b24ee47..000000000000 --- a/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedValidation.Core; - -namespace SeedValidation.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/validation/src/SeedValidation/Core/StringEnumSerializer.cs b/seed/csharp-model/validation/src/SeedValidation/Core/StringEnumSerializer.cs deleted file mode 100644 index 855d6f1459a1..000000000000 --- a/seed/csharp-model/validation/src/SeedValidation/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedValidation.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/validation/src/SeedValidation/Shape.cs b/seed/csharp-model/validation/src/SeedValidation/Shape.cs index 8532b678c37b..10d218ab08ea 100644 --- a/seed/csharp-model/validation/src/SeedValidation/Shape.cs +++ b/seed/csharp-model/validation/src/SeedValidation/Shape.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedValidation.Core; namespace SeedValidation; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Shape.ShapeSerializer))] [Serializable] public readonly record struct Shape : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator Shape(string value) => new(value); + internal class ShapeSerializer : JsonConverter + { + public override Shape Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Shape(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Shape value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 749a3596736c..000000000000 --- a/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedVariables.Core; - -namespace SeedVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/variables/src/SeedVariables/Core/StringEnumSerializer.cs b/seed/csharp-model/variables/src/SeedVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index 79966dd7c0df..000000000000 --- a/seed/csharp-model/variables/src/SeedVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3e19afddc513..000000000000 --- a/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedVersion.Core; - -namespace SeedVersion.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs b/seed/csharp-model/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs deleted file mode 100644 index 2fc86e4fe8e4..000000000000 --- a/seed/csharp-model/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedVersion.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3e19afddc513..000000000000 --- a/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedVersion.Core; - -namespace SeedVersion.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/version/src/SeedVersion/Core/StringEnumSerializer.cs b/seed/csharp-model/version/src/SeedVersion/Core/StringEnumSerializer.cs deleted file mode 100644 index 2fc86e4fe8e4..000000000000 --- a/seed/csharp-model/version/src/SeedVersion/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedVersion.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-model/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-model/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-model/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 928efb0a1f1f..000000000000 --- a/seed/csharp-model/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebhooks.Core; - -namespace SeedWebhooks.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs b/seed/csharp-model/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs deleted file mode 100644 index e9b458d13315..000000000000 --- a/seed/csharp-model/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebhooks.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e7dc182cc86b..000000000000 --- a/seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebsocketBearerAuth.Core; - -namespace SeedWebsocketBearerAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs b/seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index cc91017243f0..000000000000 --- a/seed/csharp-model/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebsocketBearerAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3d79105fa6cf..000000000000 --- a/seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebsocketAuth.Core; - -namespace SeedWebsocketAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs b/seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 7cf74e4e8397..000000000000 --- a/seed/csharp-model/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebsocketAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b43a235f4587..000000000000 --- a/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebsocket.Core; - -namespace SeedWebsocket.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-model/websocket/src/SeedWebsocket/Core/StringEnumSerializer.cs b/seed/csharp-model/websocket/src/SeedWebsocket/Core/StringEnumSerializer.cs deleted file mode 100644 index 7e0887f1b58c..000000000000 --- a/seed/csharp-model/websocket/src/SeedWebsocket/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebsocket.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} From 4197f923110e959a69e4eda18a541f4ea36d820d Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:35:46 -0400 Subject: [PATCH 10/29] chore(csharp): update csharp-sdk seed (#13527) Co-authored-by: fern-support --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedAccept/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedAlias/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedAnyAuth/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Ast/Types/PrimitiveValue.cs | 29 +++- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Ast/Types/PrimitiveValue.cs | 29 +++- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../SeedApi/Dataservice/Types/IndexType.cs | 29 +++- .../src/SeedApi/Types/FieldBehavior.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../SeedApi/Dataservice/Types/IndexType.cs | 29 +++- .../src/SeedApi/Types/FieldBehavior.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../SeedApi/Dataservice/Types/IndexType.cs | 29 +++- .../src/SeedApi/Types/FieldBehavior.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../SeedApi/Dataservice/Types/IndexType.cs | 29 +++- .../src/SeedApi/Types/FieldBehavior.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../plain-enums/src/SeedEnum/Types/Color.cs | 28 ++-- .../src/SeedEnum/Types/EnumWithCustom.cs | 36 ++-- .../Types/EnumWithSpecialCharacters.cs | 36 ++-- .../SeedEnum/Types/ForwardCompatibleEnum.cs | 36 ++-- .../plain-enums/src/SeedEnum/Types/Operand.cs | 40 +++-- .../src/SeedEnum/Types/SpecialEnum.cs | 156 +++++++++--------- .../src/SeedEnum/Unknown/Types/Status.cs | 28 ++-- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedErrors/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedExamples/Core/StringEnumSerializer.cs | 25 --- .../src/SeedExamples/Types/BasicType.cs | 29 +++- .../src/SeedExamples/Types/ComplexType.cs | 29 +++- .../Types/Types/MigrationStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedExamples/Core/StringEnumSerializer.cs | 25 --- .../src/SeedExamples/Types/BasicType.cs | 29 +++- .../src/SeedExamples/Types/ComplexType.cs | 29 +++- .../Types/Types/MigrationStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedExtends/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Service/Types/ObjectType.cs | 29 +++- .../Service/Types/OpenEnumType.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedHttpHead/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Payment/Types/Currency.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedLicense/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedLicense/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedLiteral/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedLiteral/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Service/Types/ResourceStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../src/SeedMultiLineDocs/Types/Operand.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../NullableOptional/Types/UserRole.cs | 29 +++- .../NullableOptional/Types/UserStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../NullableOptional/Types/UserRole.cs | 29 +++- .../NullableOptional/Types/UserStatus.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedNullable/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedNullable/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedObject/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../File/Types/FileInfo.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../MultipleFilterSearchRequestOperator.cs | 32 +++- .../SingleFilterSearchRequestOperator.cs | 32 +++- .../Core/StringEnumSerializer.cs | 25 --- .../InlineUsers/InlineUsers/Types/Order.cs | 29 +++- .../src/SeedPagination/Users/Types/Order.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../MultipleFilterSearchRequestOperator.cs | 32 +++- .../SingleFilterSearchRequestOperator.cs | 32 +++- .../Core/StringEnumSerializer.cs | 25 --- .../InlineUsers/InlineUsers/Types/Order.cs | 29 +++- .../src/SeedPagination/Users/Types/Order.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../MultipleFilterSearchRequestOperator.cs | 32 +++- .../SingleFilterSearchRequestOperator.cs | 32 +++- .../Core/StringEnumSerializer.cs | 25 --- .../InlineUsers/InlineUsers/Types/Order.cs | 29 +++- .../src/SeedPagination/Users/Types/Order.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedTrace/Commons/Types/Language.cs | 29 +++- .../SeedTrace/Core/StringEnumSerializer.cs | 25 --- .../Migration/Types/MigrationStatus.cs | 29 +++- .../Playlist/Types/ReservedKeywordEnum.cs | 29 +++- .../Types/ExecutionSessionStatus.cs | 29 +++- .../Types/RunningSubmissionState.cs | 29 +++- .../Submission/Types/SubmissionTypeEnum.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Union/Types/KeyType.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Union/Types/KeyType.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedUnions/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedUnions/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedUnions/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../src/SeedValidation/Types/Shape.cs | 29 +++- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedVersion/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedVersion/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../src/SeedApi/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../SeedWebhooks/Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- .../Core/Json/StringEnumSerializerTests.cs | 138 ---------------- .../Core/StringEnumSerializer.cs | 25 --- 328 files changed, 1520 insertions(+), 22552 deletions(-) delete mode 100644 seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/alias/src/SeedAlias/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/circular-references/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/errors/src/SeedErrors/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/extends/src/SeedExtends/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/folders/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/object/src/SeedObject/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/trace/src/SeedTrace/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/validation/src/SeedValidation/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/variables/src/SeedVariables/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/version/src/SeedVersion/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/Core/StringEnumSerializer.cs delete mode 100644 seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/Core/StringEnumSerializer.cs diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 8a644106d764..000000000000 --- a/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAccept.Core; - -namespace SeedAccept.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs b/seed/csharp-sdk/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs deleted file mode 100644 index 7e355c53ba43..000000000000 --- a/seed/csharp-sdk/accept-header/src/SeedAccept/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAccept.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 5640fc9654b2..000000000000 --- a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAliasExtends.Core; - -namespace SeedAliasExtends.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs deleted file mode 100644 index 3f9774619a54..000000000000 --- a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAliasExtends.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 777a87c0745b..000000000000 --- a/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAlias.Core; - -namespace SeedAlias.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/alias/src/SeedAlias/Core/StringEnumSerializer.cs b/seed/csharp-sdk/alias/src/SeedAlias/Core/StringEnumSerializer.cs deleted file mode 100644 index c75270e371f2..000000000000 --- a/seed/csharp-sdk/alias/src/SeedAlias/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAlias.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 5845d3feed96..000000000000 --- a/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAnyAuth.Core; - -namespace SeedAnyAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs b/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 5b1c932d88f7..000000000000 --- a/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAnyAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 741f8bf05408..000000000000 --- a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApiWideBasePath.Core; - -namespace SeedApiWideBasePath.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs deleted file mode 100644 index e2b4f871c0c6..000000000000 --- a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApiWideBasePath.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d8d1102f3f1a..000000000000 --- a/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedAudiences.Core; - -namespace SeedAudiences.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs b/seed/csharp-sdk/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs deleted file mode 100644 index b60613707307..000000000000 --- a/seed/csharp-sdk/audiences/src/SeedAudiences/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedAudiences.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7f5e3c2b7227..000000000000 --- a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBasicAuthEnvironmentVariables.Core; - -namespace SeedBasicAuthEnvironmentVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index e175e622ebd7..000000000000 --- a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBasicAuthEnvironmentVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 240a5a8748f1..000000000000 --- a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBasicAuth.Core; - -namespace SeedBasicAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index bb209f14a8d5..000000000000 --- a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBasicAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3903298a97a6..000000000000 --- a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBearerTokenEnvironmentVariable.Core; - -namespace SeedBearerTokenEnvironmentVariable.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs deleted file mode 100644 index 843499ec1ed4..000000000000 --- a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBearerTokenEnvironmentVariable.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 967ac07fedbf..000000000000 --- a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBytesDownload.Core; - -namespace SeedBytesDownload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs b/seed/csharp-sdk/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs deleted file mode 100644 index 38e325564436..000000000000 --- a/seed/csharp-sdk/bytes-download/src/SeedBytesDownload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBytesDownload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 212f8eabdcc3..000000000000 --- a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedBytesUpload.Core; - -namespace SeedBytesUpload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs b/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs deleted file mode 100644 index 18200067ff66..000000000000 --- a/seed/csharp-sdk/bytes-upload/src/SeedBytesUpload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedBytesUpload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Ast/Types/PrimitiveValue.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Ast/Types/PrimitiveValue.cs index 04cb020eb5c0..92a7b6f91f86 100644 --- a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Ast/Types/PrimitiveValue.cs +++ b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Ast/Types/PrimitiveValue.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(PrimitiveValue.PrimitiveValueSerializer))] [Serializable] public readonly record struct PrimitiveValue : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator PrimitiveValue(string value) => new(value); + internal class PrimitiveValueSerializer : JsonConverter + { + public override PrimitiveValue Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new PrimitiveValue(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + PrimitiveValue value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/circular-references/src/SeedApi/Ast/Types/PrimitiveValue.cs b/seed/csharp-sdk/circular-references/src/SeedApi/Ast/Types/PrimitiveValue.cs index 04cb020eb5c0..92a7b6f91f86 100644 --- a/seed/csharp-sdk/circular-references/src/SeedApi/Ast/Types/PrimitiveValue.cs +++ b/seed/csharp-sdk/circular-references/src/SeedApi/Ast/Types/PrimitiveValue.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(PrimitiveValue.PrimitiveValueSerializer))] [Serializable] public readonly record struct PrimitiveValue : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator PrimitiveValue(string value) => new(value); + internal class PrimitiveValueSerializer : JsonConverter + { + public override PrimitiveValue Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new PrimitiveValue(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + PrimitiveValue value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/circular-references/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/circular-references/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/circular-references/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 92795eda1ee0..000000000000 --- a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedClientSideParams.Core; - -namespace SeedClientSideParams.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs b/seed/csharp-sdk/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs deleted file mode 100644 index 644105e8917a..000000000000 --- a/seed/csharp-sdk/client-side-params/src/SeedClientSideParams/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedClientSideParams.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b4e40dca3030..000000000000 --- a/seed/csharp-sdk/content-type/src/SeedContentTypes.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedContentTypes.Core; - -namespace SeedContentTypes.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs b/seed/csharp-sdk/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs deleted file mode 100644 index 3fd0bb635906..000000000000 --- a/seed/csharp-sdk/content-type/src/SeedContentTypes/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedContentTypes.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bacc5e8795ff..000000000000 --- a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCrossPackageTypeNames.Core; - -namespace SeedCrossPackageTypeNames.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs deleted file mode 100644 index ef28adebef97..000000000000 --- a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCrossPackageTypeNames.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs index efdca475318f..05b6fe6d5ff9 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Dataservice/Types/IndexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(IndexType.IndexTypeSerializer))] [Serializable] public readonly record struct IndexType : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator IndexType(string value) => new(value); + internal class IndexTypeSerializer : JsonConverter + { + public override IndexType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new IndexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + IndexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs index 38cc250adbcf..a291319f57db 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Types/FieldBehavior.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(FieldBehavior.FieldBehaviorSerializer))] [Serializable] public readonly record struct FieldBehavior : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator FieldBehavior(string value) => new(value); + internal class FieldBehaviorSerializer : JsonConverter + { + public override FieldBehavior Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new FieldBehavior(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + FieldBehavior value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs index efdca475318f..05b6fe6d5ff9 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Dataservice/Types/IndexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(IndexType.IndexTypeSerializer))] [Serializable] public readonly record struct IndexType : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator IndexType(string value) => new(value); + internal class IndexTypeSerializer : JsonConverter + { + public override IndexType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new IndexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + IndexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs index 38cc250adbcf..a291319f57db 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Types/FieldBehavior.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(FieldBehavior.FieldBehaviorSerializer))] [Serializable] public readonly record struct FieldBehavior : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator FieldBehavior(string value) => new(value); + internal class FieldBehaviorSerializer : JsonConverter + { + public override FieldBehavior Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new FieldBehavior(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + FieldBehavior value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs index efdca475318f..05b6fe6d5ff9 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Dataservice/Types/IndexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(IndexType.IndexTypeSerializer))] [Serializable] public readonly record struct IndexType : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator IndexType(string value) => new(value); + internal class IndexTypeSerializer : JsonConverter + { + public override IndexType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new IndexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + IndexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs index 38cc250adbcf..a291319f57db 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Types/FieldBehavior.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(FieldBehavior.FieldBehaviorSerializer))] [Serializable] public readonly record struct FieldBehavior : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator FieldBehavior(string value) => new(value); + internal class FieldBehaviorSerializer : JsonConverter + { + public override FieldBehavior Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new FieldBehavior(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + FieldBehavior value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs index efdca475318f..05b6fe6d5ff9 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Dataservice/Types/IndexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(IndexType.IndexTypeSerializer))] [Serializable] public readonly record struct IndexType : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator IndexType(string value) => new(value); + internal class IndexTypeSerializer : JsonConverter + { + public override IndexType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new IndexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + IndexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs index 38cc250adbcf..a291319f57db 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Types/FieldBehavior.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedApi.Core; namespace SeedApi; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(FieldBehavior.FieldBehaviorSerializer))] [Serializable] public readonly record struct FieldBehavior : IStringEnum { @@ -68,6 +69,32 @@ public override string ToString() public static explicit operator FieldBehavior(string value) => new(value); + internal class FieldBehaviorSerializer : JsonConverter + { + public override FieldBehavior Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new FieldBehavior(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + FieldBehavior value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/csharp-grpc-proto/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 344d34ec5a30..000000000000 --- a/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpNamespaceCollision.Core; - -namespace SeedCsharpNamespaceCollision.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs deleted file mode 100644 index 2b651998b1fa..000000000000 --- a/seed/csharp-sdk/csharp-namespace-collision/fully-qualified-namespaces/src/SeedCsharpNamespaceCollision/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace SeedCsharpNamespaceCollision.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index ce48e4ca8e55..000000000000 --- a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpNamespaceConflict.Core; - -namespace SeedCsharpNamespaceConflict.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs deleted file mode 100644 index f04938b2225c..000000000000 --- a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpNamespaceConflict.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 80a0d8d5cacb..000000000000 --- a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpReadonlyRequest.Core; - -namespace SeedCsharpReadonlyRequest.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs deleted file mode 100644 index 0184add58312..000000000000 --- a/seed/csharp-sdk/csharp-readonly-request/src/SeedCsharpReadonlyRequest/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpReadonlyRequest.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 551aec342853..000000000000 --- a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpSystemCollision.Core; - -namespace SeedCsharpSystemCollision.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs deleted file mode 100644 index b0ed5550cfa4..000000000000 --- a/seed/csharp-sdk/csharp-system-collision/system-client/src/SeedCsharpSystemCollision/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpSystemCollision.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index dba8b129db8f..000000000000 --- a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedCsharpXmlEntities.Core; - -namespace SeedCsharpXmlEntities.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs b/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs deleted file mode 100644 index 10b2e9349ab8..000000000000 --- a/seed/csharp-sdk/csharp-xml-entities/no-custom-config/src/SeedCsharpXmlEntities/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedCsharpXmlEntities.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 5fa48d30deff..000000000000 --- a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedDollarStringExamples.Core; - -namespace SeedDollarStringExamples.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs b/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs deleted file mode 100644 index 6f3607e5000f..000000000000 --- a/seed/csharp-sdk/dollar-string-examples/src/SeedDollarStringExamples/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedDollarStringExamples.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d94e172d938b..000000000000 --- a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEmptyClients.Core; - -namespace SeedEmptyClients.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs b/seed/csharp-sdk/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs deleted file mode 100644 index bc9e216c4731..000000000000 --- a/seed/csharp-sdk/empty-clients/src/SeedEmptyClients/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEmptyClients.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index cc6bec4aa072..000000000000 --- a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedEndpointSecurityAuth.Core; - -namespace SeedEndpointSecurityAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs b/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 6227557247b4..000000000000 --- a/seed/csharp-sdk/endpoint-security-auth/src/SeedEndpointSecurityAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedEndpointSecurityAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs index 163c8232b435..8bd7be58e81b 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Color.cs @@ -15,6 +15,16 @@ public enum Color internal class ColorSerializer : global::System.Text.Json.Serialization.JsonConverter { + private static readonly global::System.Collections.Generic.Dictionary< + string, + Color + > _stringToEnum = new() { { "red", Color.Red }, { "blue", Color.Blue } }; + + private static readonly global::System.Collections.Generic.Dictionary< + Color, + string + > _enumToString = new() { { Color.Red, "red" }, { Color.Blue, "blue" } }; + public override Color Read( ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, @@ -24,12 +34,7 @@ public override Color Read( var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return stringValue switch - { - "red" => Color.Red, - "blue" => Color.Blue, - _ => default, - }; + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; } public override void Write( @@ -39,16 +44,7 @@ public override void Write( ) { writer.WriteStringValue( - value switch - { - Color.Red => "red", - Color.Blue => "blue", - _ => throw new global::System.ArgumentOutOfRangeException( - nameof(value), - value, - null - ), - } + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null ); } } diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs index f026f9e20285..1250d49d8a1a 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithCustom.cs @@ -16,6 +16,24 @@ public enum EnumWithCustom internal class EnumWithCustomSerializer : global::System.Text.Json.Serialization.JsonConverter { + private static readonly global::System.Collections.Generic.Dictionary< + string, + EnumWithCustom + > _stringToEnum = new() + { + { "safe", EnumWithCustom.Safe }, + { "Custom", EnumWithCustom.Custom }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + EnumWithCustom, + string + > _enumToString = new() + { + { EnumWithCustom.Safe, "safe" }, + { EnumWithCustom.Custom, "Custom" }, + }; + public override EnumWithCustom Read( ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, @@ -25,12 +43,7 @@ public override EnumWithCustom Read( var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return stringValue switch - { - "safe" => EnumWithCustom.Safe, - "Custom" => EnumWithCustom.Custom, - _ => default, - }; + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; } public override void Write( @@ -40,16 +53,7 @@ public override void Write( ) { writer.WriteStringValue( - value switch - { - EnumWithCustom.Safe => "safe", - EnumWithCustom.Custom => "Custom", - _ => throw new global::System.ArgumentOutOfRangeException( - nameof(value), - value, - null - ), - } + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null ); } } diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs index 776268be6844..7825d88d953d 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/EnumWithSpecialCharacters.cs @@ -16,6 +16,24 @@ public enum EnumWithSpecialCharacters internal class EnumWithSpecialCharactersSerializer : global::System.Text.Json.Serialization.JsonConverter { + private static readonly global::System.Collections.Generic.Dictionary< + string, + EnumWithSpecialCharacters + > _stringToEnum = new() + { + { "\\$bla", EnumWithSpecialCharacters.Bla }, + { "\\$yo", EnumWithSpecialCharacters.Yo }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + EnumWithSpecialCharacters, + string + > _enumToString = new() + { + { EnumWithSpecialCharacters.Bla, "\\$bla" }, + { EnumWithSpecialCharacters.Yo, "\\$yo" }, + }; + public override EnumWithSpecialCharacters Read( ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, @@ -25,12 +43,7 @@ public override EnumWithSpecialCharacters Read( var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return stringValue switch - { - "\\$bla" => EnumWithSpecialCharacters.Bla, - "\\$yo" => EnumWithSpecialCharacters.Yo, - _ => default, - }; + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; } public override void Write( @@ -40,16 +53,7 @@ public override void Write( ) { writer.WriteStringValue( - value switch - { - EnumWithSpecialCharacters.Bla => "\\$bla", - EnumWithSpecialCharacters.Yo => "\\$yo", - _ => throw new global::System.ArgumentOutOfRangeException( - nameof(value), - value, - null - ), - } + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null ); } } diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs index 66e4dd179dee..a35a4625674f 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/ForwardCompatibleEnum.cs @@ -16,6 +16,24 @@ public enum ForwardCompatibleEnum internal class ForwardCompatibleEnumSerializer : global::System.Text.Json.Serialization.JsonConverter { + private static readonly global::System.Collections.Generic.Dictionary< + string, + ForwardCompatibleEnum + > _stringToEnum = new() + { + { "active", ForwardCompatibleEnum.Active }, + { "inactive", ForwardCompatibleEnum.Inactive }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + ForwardCompatibleEnum, + string + > _enumToString = new() + { + { ForwardCompatibleEnum.Active, "active" }, + { ForwardCompatibleEnum.Inactive, "inactive" }, + }; + public override ForwardCompatibleEnum Read( ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, @@ -25,12 +43,7 @@ public override ForwardCompatibleEnum Read( var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return stringValue switch - { - "active" => ForwardCompatibleEnum.Active, - "inactive" => ForwardCompatibleEnum.Inactive, - _ => default, - }; + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; } public override void Write( @@ -40,16 +53,7 @@ public override void Write( ) { writer.WriteStringValue( - value switch - { - ForwardCompatibleEnum.Active => "active", - ForwardCompatibleEnum.Inactive => "inactive", - _ => throw new global::System.ArgumentOutOfRangeException( - nameof(value), - value, - null - ), - } + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null ); } } diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs index ef1be49f33cf..57936aa1fdb7 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/Operand.cs @@ -18,6 +18,26 @@ public enum Operand internal class OperandSerializer : global::System.Text.Json.Serialization.JsonConverter { + private static readonly global::System.Collections.Generic.Dictionary< + string, + Operand + > _stringToEnum = new() + { + { ">", Operand.GreaterThan }, + { "=", Operand.EqualTo }, + { "less_than", Operand.LessThan }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + Operand, + string + > _enumToString = new() + { + { Operand.GreaterThan, ">" }, + { Operand.EqualTo, "=" }, + { Operand.LessThan, "less_than" }, + }; + public override Operand Read( ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, @@ -27,13 +47,7 @@ public override Operand Read( var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return stringValue switch - { - ">" => Operand.GreaterThan, - "=" => Operand.EqualTo, - "less_than" => Operand.LessThan, - _ => default, - }; + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; } public override void Write( @@ -43,17 +57,7 @@ public override void Write( ) { writer.WriteStringValue( - value switch - { - Operand.GreaterThan => ">", - Operand.EqualTo => "=", - Operand.LessThan => "less_than", - _ => throw new global::System.ArgumentOutOfRangeException( - nameof(value), - value, - null - ), - } + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null ); } } diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs index c99561192bd9..d19e6d25361b 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Types/SpecialEnum.cs @@ -106,6 +106,84 @@ public enum SpecialEnum internal class SpecialEnumSerializer : global::System.Text.Json.Serialization.JsonConverter { + private static readonly global::System.Collections.Generic.Dictionary< + string, + SpecialEnum + > _stringToEnum = new() + { + { "", SpecialEnum.A }, + { "Hello \\\"World\\\"", SpecialEnum.B }, + { "Hello 'World'", SpecialEnum.C }, + { "Hello\\\\World", SpecialEnum.D }, + { "Hello\\nWorld", SpecialEnum.E }, + { "Hello\\rWorld", SpecialEnum.F }, + { "Hello\\tWorld", SpecialEnum.H }, + { "Hello\\fWorld", SpecialEnum.I }, + { "Hello\\u0008World", SpecialEnum.J }, + { "Hello\\vWorld", SpecialEnum.K }, + { "Hello\\x00World", SpecialEnum.L }, + { "Hello\\u0007World", SpecialEnum.M }, + { "Hello\\u0001World", SpecialEnum.N }, + { "Hello\\u0002World", SpecialEnum.O }, + { "Hello\\u001FWorld", SpecialEnum.P }, + { "Hello\\u007FWorld", SpecialEnum.Q }, + { "Hello\\u009FWorld", SpecialEnum.R }, + { "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null", SpecialEnum.S }, + { "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007", SpecialEnum.T }, + { "Hello 世界", SpecialEnum.U }, + { "café", SpecialEnum.V }, + { "🚀", SpecialEnum.W }, + { "\\\\n", SpecialEnum.X }, + { "\\\\", SpecialEnum.Y }, + { "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}", SpecialEnum.Z }, + { "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'", SpecialEnum.Aa }, + { "C:\\\\Users\\\\John\\\\Documents\\\\file.txt", SpecialEnum.Bb }, + { "/usr/local/bin/app", SpecialEnum.Cc }, + { "\\\\d{3}-\\\\d{2}-\\\\d{4}", SpecialEnum.Dd }, + { "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}", SpecialEnum.Ee }, + { "transcript[transcriptType=\"final\"]", SpecialEnum.Ff }, + { "transcript[transcriptType='final']", SpecialEnum.Gg }, + }; + + private static readonly global::System.Collections.Generic.Dictionary< + SpecialEnum, + string + > _enumToString = new() + { + { SpecialEnum.A, "" }, + { SpecialEnum.B, "Hello \\\"World\\\"" }, + { SpecialEnum.C, "Hello 'World'" }, + { SpecialEnum.D, "Hello\\\\World" }, + { SpecialEnum.E, "Hello\\nWorld" }, + { SpecialEnum.F, "Hello\\rWorld" }, + { SpecialEnum.H, "Hello\\tWorld" }, + { SpecialEnum.I, "Hello\\fWorld" }, + { SpecialEnum.J, "Hello\\u0008World" }, + { SpecialEnum.K, "Hello\\vWorld" }, + { SpecialEnum.L, "Hello\\x00World" }, + { SpecialEnum.M, "Hello\\u0007World" }, + { SpecialEnum.N, "Hello\\u0001World" }, + { SpecialEnum.O, "Hello\\u0002World" }, + { SpecialEnum.P, "Hello\\u001FWorld" }, + { SpecialEnum.Q, "Hello\\u007FWorld" }, + { SpecialEnum.R, "Hello\\u009FWorld" }, + { SpecialEnum.S, "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null" }, + { SpecialEnum.T, "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007" }, + { SpecialEnum.U, "Hello 世界" }, + { SpecialEnum.V, "café" }, + { SpecialEnum.W, "🚀" }, + { SpecialEnum.X, "\\\\n" }, + { SpecialEnum.Y, "\\\\" }, + { SpecialEnum.Z, "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}" }, + { SpecialEnum.Aa, "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'" }, + { SpecialEnum.Bb, "C:\\\\Users\\\\John\\\\Documents\\\\file.txt" }, + { SpecialEnum.Cc, "/usr/local/bin/app" }, + { SpecialEnum.Dd, "\\\\d{3}-\\\\d{2}-\\\\d{4}" }, + { SpecialEnum.Ee, "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}" }, + { SpecialEnum.Ff, "transcript[transcriptType=\"final\"]" }, + { SpecialEnum.Gg, "transcript[transcriptType='final']" }, + }; + public override SpecialEnum Read( ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, @@ -115,42 +193,7 @@ public override SpecialEnum Read( var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return stringValue switch - { - "" => SpecialEnum.A, - "Hello \\\"World\\\"" => SpecialEnum.B, - "Hello 'World'" => SpecialEnum.C, - "Hello\\\\World" => SpecialEnum.D, - "Hello\\nWorld" => SpecialEnum.E, - "Hello\\rWorld" => SpecialEnum.F, - "Hello\\tWorld" => SpecialEnum.H, - "Hello\\fWorld" => SpecialEnum.I, - "Hello\\u0008World" => SpecialEnum.J, - "Hello\\vWorld" => SpecialEnum.K, - "Hello\\x00World" => SpecialEnum.L, - "Hello\\u0007World" => SpecialEnum.M, - "Hello\\u0001World" => SpecialEnum.N, - "Hello\\u0002World" => SpecialEnum.O, - "Hello\\u001FWorld" => SpecialEnum.P, - "Hello\\u007FWorld" => SpecialEnum.Q, - "Hello\\u009FWorld" => SpecialEnum.R, - "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null" => SpecialEnum.S, - "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007" => SpecialEnum.T, - "Hello 世界" => SpecialEnum.U, - "café" => SpecialEnum.V, - "🚀" => SpecialEnum.W, - "\\\\n" => SpecialEnum.X, - "\\\\" => SpecialEnum.Y, - "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}" => SpecialEnum.Z, - "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'" => SpecialEnum.Aa, - "C:\\\\Users\\\\John\\\\Documents\\\\file.txt" => SpecialEnum.Bb, - "/usr/local/bin/app" => SpecialEnum.Cc, - "\\\\d{3}-\\\\d{2}-\\\\d{4}" => SpecialEnum.Dd, - "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}" => SpecialEnum.Ee, - "transcript[transcriptType=\"final\"]" => SpecialEnum.Ff, - "transcript[transcriptType='final']" => SpecialEnum.Gg, - _ => default, - }; + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; } public override void Write( @@ -160,46 +203,7 @@ public override void Write( ) { writer.WriteStringValue( - value switch - { - SpecialEnum.A => "", - SpecialEnum.B => "Hello \\\"World\\\"", - SpecialEnum.C => "Hello 'World'", - SpecialEnum.D => "Hello\\\\World", - SpecialEnum.E => "Hello\\nWorld", - SpecialEnum.F => "Hello\\rWorld", - SpecialEnum.H => "Hello\\tWorld", - SpecialEnum.I => "Hello\\fWorld", - SpecialEnum.J => "Hello\\u0008World", - SpecialEnum.K => "Hello\\vWorld", - SpecialEnum.L => "Hello\\x00World", - SpecialEnum.M => "Hello\\u0007World", - SpecialEnum.N => "Hello\\u0001World", - SpecialEnum.O => "Hello\\u0002World", - SpecialEnum.P => "Hello\\u001FWorld", - SpecialEnum.Q => "Hello\\u007FWorld", - SpecialEnum.R => "Hello\\u009FWorld", - SpecialEnum.S => "Line 1\\n\"Quote\"\\tTab\\\\Backslash\\r\\nLine 2\\0Null", - SpecialEnum.T => "\\n\\r\\t\\x00\\u0008\\f\\v\\u0007", - SpecialEnum.U => "Hello 世界", - SpecialEnum.V => "café", - SpecialEnum.W => "🚀", - SpecialEnum.X => "\\\\n", - SpecialEnum.Y => "\\\\", - SpecialEnum.Z => "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}", - SpecialEnum.Aa => "SELECT * FROM users WHERE name = 'John O\\\\'Reilly'", - SpecialEnum.Bb => "C:\\\\Users\\\\John\\\\Documents\\\\file.txt", - SpecialEnum.Cc => "/usr/local/bin/app", - SpecialEnum.Dd => "\\\\d{3}-\\\\d{2}-\\\\d{4}", - SpecialEnum.Ee => "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}", - SpecialEnum.Ff => "transcript[transcriptType=\"final\"]", - SpecialEnum.Gg => "transcript[transcriptType='final']", - _ => throw new global::System.ArgumentOutOfRangeException( - nameof(value), - value, - null - ), - } + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null ); } } diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs index d30564718676..2927f5d84b99 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Unknown/Types/Status.cs @@ -15,6 +15,16 @@ public enum Status internal class StatusSerializer : global::System.Text.Json.Serialization.JsonConverter { + private static readonly global::System.Collections.Generic.Dictionary< + string, + Status + > _stringToEnum = new() { { "Known", Status.Known }, { "Unknown", Status.Unknown } }; + + private static readonly global::System.Collections.Generic.Dictionary< + Status, + string + > _enumToString = new() { { Status.Known, "Known" }, { Status.Unknown, "Unknown" } }; + public override Status Read( ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, @@ -24,12 +34,7 @@ public override Status Read( var stringValue = reader.GetString() ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return stringValue switch - { - "Known" => Status.Known, - "Unknown" => Status.Unknown, - _ => default, - }; + return _stringToEnum.TryGetValue(stringValue, out var enumValue) ? enumValue : default; } public override void Write( @@ -39,16 +44,7 @@ public override void Write( ) { writer.WriteStringValue( - value switch - { - Status.Known => "Known", - Status.Unknown => "Unknown", - _ => throw new global::System.ArgumentOutOfRangeException( - nameof(value), - value, - null - ), - } + _enumToString.TryGetValue(value, out var stringValue) ? stringValue : null ); } } diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index df9da3401d28..000000000000 --- a/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedErrorProperty.Core; - -namespace SeedErrorProperty.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs deleted file mode 100644 index 0957a02c99d2..000000000000 --- a/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedErrorProperty.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 94f333d30865..000000000000 --- a/seed/csharp-sdk/errors/src/SeedErrors.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedErrors.Core; - -namespace SeedErrors.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/errors/src/SeedErrors/Core/StringEnumSerializer.cs b/seed/csharp-sdk/errors/src/SeedErrors/Core/StringEnumSerializer.cs deleted file mode 100644 index 02a5777c4dca..000000000000 --- a/seed/csharp-sdk/errors/src/SeedErrors/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedErrors.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f43b68408195..000000000000 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExamples.Core; - -namespace SeedExamples.Test_.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/StringEnumSerializer.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/StringEnumSerializer.cs deleted file mode 100644 index 7c04fcb7cf4e..000000000000 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExamples.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/BasicType.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/BasicType.cs index f913ec1dadcc..f061512fe2bd 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/BasicType.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/BasicType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(BasicType.BasicTypeSerializer))] [Serializable] public readonly record struct BasicType : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator BasicType(string value) => new(value); + internal class BasicTypeSerializer : JsonConverter + { + public override BasicType Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new BasicType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + BasicType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/ComplexType.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/ComplexType.cs index 8516776b91dc..7fdeabb1f778 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/ComplexType.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/ComplexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ComplexType.ComplexTypeSerializer))] [Serializable] public readonly record struct ComplexType : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ComplexType(string value) => new(value); + internal class ComplexTypeSerializer : JsonConverter + { + public override ComplexType Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ComplexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ComplexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/Types/MigrationStatus.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/Types/MigrationStatus.cs index 0656831be6bb..69739723cfe9 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/Types/MigrationStatus.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Types/Types/MigrationStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(MigrationStatus.MigrationStatusSerializer))] [Serializable] public readonly record struct MigrationStatus : IStringEnum { @@ -60,6 +61,32 @@ public override string ToString() public static explicit operator MigrationStatus(string value) => new(value); + internal class MigrationStatusSerializer : JsonConverter + { + public override MigrationStatus Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MigrationStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MigrationStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f43b68408195..000000000000 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExamples.Core; - -namespace SeedExamples.Test_.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/StringEnumSerializer.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/StringEnumSerializer.cs deleted file mode 100644 index 7c04fcb7cf4e..000000000000 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExamples.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/BasicType.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/BasicType.cs index f913ec1dadcc..f061512fe2bd 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/BasicType.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/BasicType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(BasicType.BasicTypeSerializer))] [Serializable] public readonly record struct BasicType : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator BasicType(string value) => new(value); + internal class BasicTypeSerializer : JsonConverter + { + public override BasicType Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new BasicType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + BasicType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/ComplexType.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/ComplexType.cs index 8516776b91dc..7fdeabb1f778 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/ComplexType.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/ComplexType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ComplexType.ComplexTypeSerializer))] [Serializable] public readonly record struct ComplexType : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator ComplexType(string value) => new(value); + internal class ComplexTypeSerializer : JsonConverter + { + public override ComplexType Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ComplexType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ComplexType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/Types/MigrationStatus.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/Types/MigrationStatus.cs index 0656831be6bb..69739723cfe9 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/Types/MigrationStatus.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Types/Types/MigrationStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedExamples.Core; namespace SeedExamples; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(MigrationStatus.MigrationStatusSerializer))] [Serializable] public readonly record struct MigrationStatus : IStringEnum { @@ -60,6 +61,32 @@ public override string ToString() public static explicit operator MigrationStatus(string value) => new(value); + internal class MigrationStatusSerializer : JsonConverter + { + public override MigrationStatus Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MigrationStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MigrationStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index c2ca469ffefc..000000000000 --- a/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExtends.Core; - -namespace SeedExtends.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/extends/src/SeedExtends/Core/StringEnumSerializer.cs b/seed/csharp-sdk/extends/src/SeedExtends/Core/StringEnumSerializer.cs deleted file mode 100644 index fc76c249fb2c..000000000000 --- a/seed/csharp-sdk/extends/src/SeedExtends/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExtends.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e9dc842d9c71..000000000000 --- a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedExtraProperties.Core; - -namespace SeedExtraProperties.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs deleted file mode 100644 index 0896fa563630..000000000000 --- a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedExtraProperties.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 33a2be57c538..000000000000 --- a/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedFileDownload.Core; - -namespace SeedFileDownload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs deleted file mode 100644 index d0995cbaacaf..000000000000 --- a/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedFileDownload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/file-upload-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/file-upload-openapi/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 1e40f9937f4a..000000000000 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedFileUpload.Core; - -namespace SeedFileUpload.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs deleted file mode 100644 index 64a023082141..000000000000 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedFileUpload.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/ObjectType.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/ObjectType.cs index e9124b25ec94..a4e2ff643b7d 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/ObjectType.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/ObjectType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedFileUpload.Core; namespace SeedFileUpload; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ObjectType.ObjectTypeSerializer))] [Serializable] public readonly record struct ObjectType : IStringEnum { @@ -51,6 +52,32 @@ public override string ToString() public static explicit operator ObjectType(string value) => new(value); + internal class ObjectTypeSerializer : JsonConverter + { + public override ObjectType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ObjectType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ObjectType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/OpenEnumType.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/OpenEnumType.cs index 18257d195d2a..97e49152794d 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/OpenEnumType.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Types/OpenEnumType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedFileUpload.Core; namespace SeedFileUpload; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(OpenEnumType.OpenEnumTypeSerializer))] [Serializable] public readonly record struct OpenEnumType : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator OpenEnumType(string value) => new(value); + internal class OpenEnumTypeSerializer : JsonConverter + { + public override OpenEnumType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new OpenEnumType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + OpenEnumType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/folders/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/folders/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/folders/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 9af5ccf52cba..000000000000 --- a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedHeaderTokenEnvironmentVariable.Core; - -namespace SeedHeaderTokenEnvironmentVariable.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs b/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs deleted file mode 100644 index 0488a58c6d46..000000000000 --- a/seed/csharp-sdk/header-auth-environment-variable/src/SeedHeaderTokenEnvironmentVariable/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedHeaderTokenEnvironmentVariable.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 21eeab0ef3fa..000000000000 --- a/seed/csharp-sdk/header-auth/src/SeedHeaderToken.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedHeaderToken.Core; - -namespace SeedHeaderToken.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs b/seed/csharp-sdk/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs deleted file mode 100644 index 9e227b9d6f60..000000000000 --- a/seed/csharp-sdk/header-auth/src/SeedHeaderToken/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedHeaderToken.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 95f0c75902a2..000000000000 --- a/seed/csharp-sdk/http-head/src/SeedHttpHead.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedHttpHead.Core; - -namespace SeedHttpHead.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs b/seed/csharp-sdk/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs deleted file mode 100644 index 82d2fe43da3b..000000000000 --- a/seed/csharp-sdk/http-head/src/SeedHttpHead/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedHttpHead.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index ac56a14a1709..000000000000 --- a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedIdempotencyHeaders.Core; - -namespace SeedIdempotencyHeaders.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs deleted file mode 100644 index 35ba57b65c17..000000000000 --- a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedIdempotencyHeaders.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Types/Currency.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Types/Currency.cs index ae848aa7a451..56ca6f19e1c9 100644 --- a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Types/Currency.cs +++ b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Payment/Types/Currency.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedIdempotencyHeaders.Core; namespace SeedIdempotencyHeaders; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Currency.CurrencySerializer))] [Serializable] public readonly record struct Currency : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Currency(string value) => new(value); + internal class CurrencySerializer : JsonConverter + { + public override Currency Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Currency(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Currency value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/imdb/omit-fern-headers/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 2a06c7fe6c39..000000000000 --- a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthExplicit.Core; - -namespace SeedInferredAuthExplicit.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs b/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs deleted file mode 100644 index 2796a6d0f563..000000000000 --- a/seed/csharp-sdk/inferred-auth-explicit/src/SeedInferredAuthExplicit/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthExplicit.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7298734f93bc..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicitApiKey.Core; - -namespace SeedInferredAuthImplicitApiKey.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs b/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs deleted file mode 100644 index 2ee535db7e62..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit-api-key/src/SeedInferredAuthImplicitApiKey/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicitApiKey.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 6c66465228af..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicitNoExpiry.Core; - -namespace SeedInferredAuthImplicitNoExpiry.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs b/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs deleted file mode 100644 index bc96fa39117c..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit-no-expiry/src/SeedInferredAuthImplicitNoExpiry/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicitNoExpiry.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 728bd7d4e345..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicit.Core; - -namespace SeedInferredAuthImplicit.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs b/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs deleted file mode 100644 index 037819cd147e..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit-reference/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicit.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 728bd7d4e345..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedInferredAuthImplicit.Core; - -namespace SeedInferredAuthImplicit.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs b/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs deleted file mode 100644 index 037819cd147e..000000000000 --- a/seed/csharp-sdk/inferred-auth-implicit/src/SeedInferredAuthImplicit/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedInferredAuthImplicit.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7dbd34759269..000000000000 --- a/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLicense.Core; - -namespace SeedLicense.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/StringEnumSerializer.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/StringEnumSerializer.cs deleted file mode 100644 index 699c6fc6a81b..000000000000 --- a/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLicense.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7dbd34759269..000000000000 --- a/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLicense.Core; - -namespace SeedLicense.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/StringEnumSerializer.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/StringEnumSerializer.cs deleted file mode 100644 index 699c6fc6a81b..000000000000 --- a/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLicense.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a7260ecb8573..000000000000 --- a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLiteral.Core; - -namespace SeedLiteral.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/Core/StringEnumSerializer.cs b/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/Core/StringEnumSerializer.cs deleted file mode 100644 index e8edea07eba0..000000000000 --- a/seed/csharp-sdk/literal/no-custom-config/src/SeedLiteral/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLiteral.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a7260ecb8573..000000000000 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLiteral.Core; - -namespace SeedLiteral.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/Core/StringEnumSerializer.cs b/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/Core/StringEnumSerializer.cs deleted file mode 100644 index e8edea07eba0..000000000000 --- a/seed/csharp-sdk/literal/readonly-constants/src/SeedLiteral/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLiteral.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b37d3c428eee..000000000000 --- a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedLiteralsUnions.Core; - -namespace SeedLiteralsUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs b/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index 3f2a5750b025..000000000000 --- a/seed/csharp-sdk/literals-unions/src/SeedLiteralsUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedLiteralsUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index abeb13903035..000000000000 --- a/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMixedCase.Core; - -namespace SeedMixedCase.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs deleted file mode 100644 index e84ac03a9290..000000000000 --- a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMixedCase.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Service/Types/ResourceStatus.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Service/Types/ResourceStatus.cs index ec045d1864b3..3f0a60a5ad55 100644 --- a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Service/Types/ResourceStatus.cs +++ b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Service/Types/ResourceStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedMixedCase.Core; namespace SeedMixedCase; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ResourceStatus.ResourceStatusSerializer))] [Serializable] public readonly record struct ResourceStatus : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator ResourceStatus(string value) => new(value); + internal class ResourceStatusSerializer : JsonConverter + { + public override ResourceStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ResourceStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ResourceStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 50af30e8c157..000000000000 --- a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMixedFileDirectory.Core; - -namespace SeedMixedFileDirectory.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs deleted file mode 100644 index 118618d004d1..000000000000 --- a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMixedFileDirectory.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 21811a312708..000000000000 --- a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMultiLineDocs.Core; - -namespace SeedMultiLineDocs.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs deleted file mode 100644 index d74ce734bbd7..000000000000 --- a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMultiLineDocs.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Types/Operand.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Types/Operand.cs index 71940b31d01a..cfaed4e2be43 100644 --- a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Types/Operand.cs +++ b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Types/Operand.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedMultiLineDocs.Core; namespace SeedMultiLineDocs; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Operand.OperandSerializer))] [Serializable] public readonly record struct Operand : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator Operand(string value) => new(value); + internal class OperandSerializer : JsonConverter + { + public override Operand Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Operand(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Operand value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index ad80a44bb350..000000000000 --- a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMultiUrlEnvironmentNoDefault.Core; - -namespace SeedMultiUrlEnvironmentNoDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index 8686ac66af1d..000000000000 --- a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMultiUrlEnvironmentNoDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 52e7708a1622..000000000000 --- a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMultiUrlEnvironment.Core; - -namespace SeedMultiUrlEnvironment.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs b/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs deleted file mode 100644 index ceba4f5f12da..000000000000 --- a/seed/csharp-sdk/multi-url-environment/environment-class-name/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMultiUrlEnvironment.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 52e7708a1622..000000000000 --- a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedMultiUrlEnvironment.Core; - -namespace SeedMultiUrlEnvironment.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs deleted file mode 100644 index ceba4f5f12da..000000000000 --- a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedMultiUrlEnvironment.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/multiple-request-bodies/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index c72b8b5763c3..000000000000 --- a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNoEnvironment.Core; - -namespace SeedNoEnvironment.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs deleted file mode 100644 index 58e53ca58c40..000000000000 --- a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNoEnvironment.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b4bd49f112f8..000000000000 --- a/seed/csharp-sdk/no-retries/src/SeedNoRetries.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNoRetries.Core; - -namespace SeedNoRetries.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs b/seed/csharp-sdk/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs deleted file mode 100644 index 55b2ff5194a7..000000000000 --- a/seed/csharp-sdk/no-retries/src/SeedNoRetries/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNoRetries.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/nullable-allof-extends/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 31ea594b7ce9..000000000000 --- a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNullableOptional.Core; - -namespace SeedNullableOptional.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs deleted file mode 100644 index 7cf90a707479..000000000000 --- a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNullableOptional.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs index b27b033ad94c..3d3a87b97b87 100644 --- a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs +++ b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedNullableOptional.Core; namespace SeedNullableOptional; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(UserRole.UserRoleSerializer))] [Serializable] public readonly record struct UserRole : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator UserRole(string value) => new(value); + internal class UserRoleSerializer : JsonConverter + { + public override UserRole Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new UserRole(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + UserRole value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs index 08816dfbe443..961e2fd46de4 100644 --- a/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs +++ b/seed/csharp-sdk/nullable-optional/explicit-nullable-optional/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedNullableOptional.Core; namespace SeedNullableOptional; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(UserStatus.UserStatusSerializer))] [Serializable] public readonly record struct UserStatus : IStringEnum { @@ -55,6 +56,32 @@ public override string ToString() public static explicit operator UserStatus(string value) => new(value); + internal class UserStatusSerializer : JsonConverter + { + public override UserStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new UserStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + UserStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 31ea594b7ce9..000000000000 --- a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNullableOptional.Core; - -namespace SeedNullableOptional.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/Core/StringEnumSerializer.cs b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/Core/StringEnumSerializer.cs deleted file mode 100644 index 7cf90a707479..000000000000 --- a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNullableOptional.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs index b27b033ad94c..3d3a87b97b87 100644 --- a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs +++ b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserRole.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedNullableOptional.Core; namespace SeedNullableOptional; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(UserRole.UserRoleSerializer))] [Serializable] public readonly record struct UserRole : IStringEnum { @@ -54,6 +55,32 @@ public override string ToString() public static explicit operator UserRole(string value) => new(value); + internal class UserRoleSerializer : JsonConverter + { + public override UserRole Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new UserRole(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + UserRole value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs index 08816dfbe443..961e2fd46de4 100644 --- a/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs +++ b/seed/csharp-sdk/nullable-optional/no-custom-config/src/SeedNullableOptional/NullableOptional/Types/UserStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedNullableOptional.Core; namespace SeedNullableOptional; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(UserStatus.UserStatusSerializer))] [Serializable] public readonly record struct UserStatus : IStringEnum { @@ -55,6 +56,32 @@ public override string ToString() public static explicit operator UserStatus(string value) => new(value); + internal class UserStatusSerializer : JsonConverter + { + public override UserStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new UserStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + UserStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/nullable-request-body/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/nullable-request-body/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 45f53ec6a2b4..000000000000 --- a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNullable.Core; - -namespace SeedNullable.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/Core/StringEnumSerializer.cs b/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/Core/StringEnumSerializer.cs deleted file mode 100644 index 4a43207ef74b..000000000000 --- a/seed/csharp-sdk/nullable/explicit-nullable-optional/src/SeedNullable/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNullable.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 45f53ec6a2b4..000000000000 --- a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNullable.Core; - -namespace SeedNullable.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/Core/StringEnumSerializer.cs b/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/Core/StringEnumSerializer.cs deleted file mode 100644 index 4a43207ef74b..000000000000 --- a/seed/csharp-sdk/nullable/no-custom-config/src/SeedNullable/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNullable.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index fe3cda738337..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentials.Core; - -namespace SeedOauthClientCredentials.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs deleted file mode 100644 index 9c9beac155d8..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentials.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 818faaadfd8f..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsDefault.Core; - -namespace SeedOauthClientCredentialsDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index 93f05ba87bfd..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3d2d134917ff..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsEnvironmentVariables.Core; - -namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index 42b2f618f25b..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsEnvironmentVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0ddb4c5f73e6..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsMandatoryAuth.Core; - -namespace SeedOauthClientCredentialsMandatoryAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 107a227b0958..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/src/SeedOauthClientCredentialsMandatoryAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsMandatoryAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index fe3cda738337..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentials.Core; - -namespace SeedOauthClientCredentials.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs deleted file mode 100644 index 9c9beac155d8..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentials.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0b921acae0c1..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsReference.Core; - -namespace SeedOauthClientCredentialsReference.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs deleted file mode 100644 index 4cb5f9473e9f..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-reference/src/SeedOauthClientCredentialsReference/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsReference.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 223047839c5e..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentialsWithVariables.Core; - -namespace SeedOauthClientCredentialsWithVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index 6578e7b20d19..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-with-variables/src/SeedOauthClientCredentialsWithVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentialsWithVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index fe3cda738337..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentials.Core; - -namespace SeedOauthClientCredentials.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs deleted file mode 100644 index 9c9beac155d8..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentials.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index fe3cda738337..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedOauthClientCredentials.Core; - -namespace SeedOauthClientCredentials.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs deleted file mode 100644 index 9c9beac155d8..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedOauthClientCredentials.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f55a7fff3c15..000000000000 --- a/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedObject.Core; - -namespace SeedObject.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/object/src/SeedObject/Core/StringEnumSerializer.cs b/seed/csharp-sdk/object/src/SeedObject/Core/StringEnumSerializer.cs deleted file mode 100644 index 777256427c68..000000000000 --- a/seed/csharp-sdk/object/src/SeedObject/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedObject.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0177a91bd780..000000000000 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedObjectsWithImports.Core; - -namespace SeedObjectsWithImports.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs deleted file mode 100644 index 61b8664dc2b6..000000000000 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedObjectsWithImports.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/File/Types/FileInfo.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/File/Types/FileInfo.cs index c1b256342192..c332b86cf17f 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/File/Types/FileInfo.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/File/Types/FileInfo.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedObjectsWithImports.Core; namespace SeedObjectsWithImports; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(FileInfo.FileInfoSerializer))] [Serializable] public readonly record struct FileInfo : IStringEnum { @@ -56,6 +57,32 @@ public override string ToString() public static explicit operator FileInfo(string value) => new(value); + internal class FileInfoSerializer : JsonConverter + { + public override FileInfo Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new FileInfo(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + FileInfo value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0177a91bd780..000000000000 --- a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedObjectsWithImports.Core; - -namespace SeedObjectsWithImports.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs b/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs deleted file mode 100644 index 61b8664dc2b6..000000000000 --- a/seed/csharp-sdk/optional/no-custom-config/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedObjectsWithImports.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 0177a91bd780..000000000000 --- a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedObjectsWithImports.Core; - -namespace SeedObjectsWithImports.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs b/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs deleted file mode 100644 index 61b8664dc2b6..000000000000 --- a/seed/csharp-sdk/optional/simplify-object-dictionaries/src/SeedObjectsWithImports/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedObjectsWithImports.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bfbae5d43141..000000000000 --- a/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPackageYml.Core; - -namespace SeedPackageYml.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs deleted file mode 100644 index 3e94be4ad674..000000000000 --- a/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPackageYml.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3629e23bc177..000000000000 --- a/seed/csharp-sdk/pagination-custom/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPagination.Core; - -namespace SeedPagination.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs b/seed/csharp-sdk/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs deleted file mode 100644 index 7a70d0820d6c..000000000000 --- a/seed/csharp-sdk/pagination-custom/src/SeedPagination/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPagination.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3629e23bc177..000000000000 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPagination.Core; - -namespace SeedPagination.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs index b1091d0a6cd4..e54c858342f0 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(MultipleFilterSearchRequestOperator.MultipleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct MultipleFilterSearchRequestOperator : IStringEnum { @@ -53,6 +56,33 @@ public static explicit operator string(MultipleFilterSearchRequestOperator value public static explicit operator MultipleFilterSearchRequestOperator(string value) => new(value); + internal class MultipleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override MultipleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MultipleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MultipleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs index 01db9510ded4..77fe8ac1b125 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(SingleFilterSearchRequestOperator.SingleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct SingleFilterSearchRequestOperator : IStringEnum { @@ -70,6 +73,33 @@ public override string ToString() public static explicit operator SingleFilterSearchRequestOperator(string value) => new(value); + internal class SingleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override SingleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SingleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SingleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/StringEnumSerializer.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/StringEnumSerializer.cs deleted file mode 100644 index 7a70d0820d6c..000000000000 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPagination.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs index 9796bd07808f..c7d36b88358c 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination.InlineUsers; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/Types/Order.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/Types/Order.cs index aef9e7e6d2f4..99fa969b39b5 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/Types/Order.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Users/Types/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3629e23bc177..000000000000 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPagination.Core; - -namespace SeedPagination.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs index b1091d0a6cd4..e54c858342f0 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(MultipleFilterSearchRequestOperator.MultipleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct MultipleFilterSearchRequestOperator : IStringEnum { @@ -53,6 +56,33 @@ public static explicit operator string(MultipleFilterSearchRequestOperator value public static explicit operator MultipleFilterSearchRequestOperator(string value) => new(value); + internal class MultipleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override MultipleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MultipleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MultipleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs index 01db9510ded4..77fe8ac1b125 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(SingleFilterSearchRequestOperator.SingleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct SingleFilterSearchRequestOperator : IStringEnum { @@ -70,6 +73,33 @@ public override string ToString() public static explicit operator SingleFilterSearchRequestOperator(string value) => new(value); + internal class SingleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override SingleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SingleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SingleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/StringEnumSerializer.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/StringEnumSerializer.cs deleted file mode 100644 index 7a70d0820d6c..000000000000 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPagination.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs index 9796bd07808f..c7d36b88358c 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination.InlineUsers; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Users/Types/Order.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Users/Types/Order.cs index aef9e7e6d2f4..99fa969b39b5 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Users/Types/Order.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Users/Types/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3629e23bc177..000000000000 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPagination.Core; - -namespace SeedPagination.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs index b1091d0a6cd4..e54c858342f0 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/MultipleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(MultipleFilterSearchRequestOperator.MultipleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct MultipleFilterSearchRequestOperator : IStringEnum { @@ -53,6 +56,33 @@ public static explicit operator string(MultipleFilterSearchRequestOperator value public static explicit operator MultipleFilterSearchRequestOperator(string value) => new(value); + internal class MultipleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override MultipleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MultipleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MultipleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs index 01db9510ded4..77fe8ac1b125 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Complex/Types/SingleFilterSearchRequestOperator.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(SingleFilterSearchRequestOperator.SingleFilterSearchRequestOperatorSerializer) +)] [Serializable] public readonly record struct SingleFilterSearchRequestOperator : IStringEnum { @@ -70,6 +73,33 @@ public override string ToString() public static explicit operator SingleFilterSearchRequestOperator(string value) => new(value); + internal class SingleFilterSearchRequestOperatorSerializer + : JsonConverter + { + public override SingleFilterSearchRequestOperator Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SingleFilterSearchRequestOperator(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SingleFilterSearchRequestOperator value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/StringEnumSerializer.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/StringEnumSerializer.cs deleted file mode 100644 index 7a70d0820d6c..000000000000 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPagination.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs index 9796bd07808f..c7d36b88358c 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/InlineUsers/InlineUsers/Types/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination.InlineUsers; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Users/Types/Order.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Users/Types/Order.cs index aef9e7e6d2f4..99fa969b39b5 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Users/Types/Order.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Users/Types/Order.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedPagination.Core; namespace SeedPagination; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Order.OrderSerializer))] [Serializable] public readonly record struct Order : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator Order(string value) => new(value); + internal class OrderSerializer : JsonConverter + { + public override Order Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Order(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Order value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 64281f08642e..000000000000 --- a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPathParameters.Core; - -namespace SeedPathParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/StringEnumSerializer.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 328c91741063..000000000000 --- a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPathParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 64281f08642e..000000000000 --- a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPathParameters.Core; - -namespace SeedPathParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs b/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 328c91741063..000000000000 --- a/seed/csharp-sdk/path-parameters/no-inline-path-parameters/src/SeedPathParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPathParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e392e66771df..000000000000 --- a/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPlainText.Core; - -namespace SeedPlainText.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs deleted file mode 100644 index ca9f1cd9720d..000000000000 --- a/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPlainText.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 606da7723116..000000000000 --- a/seed/csharp-sdk/property-access/src/SeedPropertyAccess.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPropertyAccess.Core; - -namespace SeedPropertyAccess.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs b/seed/csharp-sdk/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs deleted file mode 100644 index c200fc0b897a..000000000000 --- a/seed/csharp-sdk/property-access/src/SeedPropertyAccess/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPropertyAccess.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 84db87ed99f3..000000000000 --- a/seed/csharp-sdk/public-object/src/SeedPublicObject.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedPublicObject.Core; - -namespace SeedPublicObject.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs b/seed/csharp-sdk/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs deleted file mode 100644 index 730ebe71fdd2..000000000000 --- a/seed/csharp-sdk/public-object/src/SeedPublicObject/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedPublicObject.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/query-parameters-openapi-as-objects/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/query-parameters-openapi/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 9c05edc6a1db..000000000000 --- a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedQueryParameters.Core; - -namespace SeedQueryParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 557e063b3e06..000000000000 --- a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedQueryParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 672c248062c6..000000000000 --- a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedRequestParameters.Core; - -namespace SeedRequestParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/Core/StringEnumSerializer.cs b/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 880a057748c0..000000000000 --- a/seed/csharp-sdk/request-parameters/no-custom-config/src/SeedRequestParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedRequestParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 672c248062c6..000000000000 --- a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedRequestParameters.Core; - -namespace SeedRequestParameters.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/Core/StringEnumSerializer.cs b/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/Core/StringEnumSerializer.cs deleted file mode 100644 index 880a057748c0..000000000000 --- a/seed/csharp-sdk/request-parameters/with-defaults/src/SeedRequestParameters/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedRequestParameters.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/required-nullable/explicit-nullable-optional/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/required-nullable/no-custom-config/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 354f6d7643f5..000000000000 --- a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedNurseryApi.Core; - -namespace SeedNurseryApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs deleted file mode 100644 index 61bafd65edb2..000000000000 --- a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedNurseryApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 06d735649ff5..000000000000 --- a/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedResponseProperty.Core; - -namespace SeedResponseProperty.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs deleted file mode 100644 index 1bcb4e6909fd..000000000000 --- a/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedResponseProperty.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bb88c0b06de0..000000000000 --- a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedServerSentEvents.Core; - -namespace SeedServerSentEvents.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs deleted file mode 100644 index a9dba578325f..000000000000 --- a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedServerSentEvents.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index bb88c0b06de0..000000000000 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedServerSentEvents.Core; - -namespace SeedServerSentEvents.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs deleted file mode 100644 index a9dba578325f..000000000000 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedServerSentEvents.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/server-url-templating/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/server-url-templating/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/Core/StringEnumSerializer.cs deleted file mode 100644 index f57d66a0377b..000000000000 --- a/seed/csharp-sdk/simple-api/custom-output-path-object/lib/SeedApi/SeedSimpleApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSimpleApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f79f10bf40eb..000000000000 --- a/seed/csharp-sdk/simple-api/custom-output-path-object/test/SeedApi.Test/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSimpleApi.Core; - -namespace SeedSimpleApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f79f10bf40eb..000000000000 --- a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSimpleApi.Core; - -namespace SeedSimpleApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/Core/StringEnumSerializer.cs deleted file mode 100644 index f57d66a0377b..000000000000 --- a/seed/csharp-sdk/simple-api/custom-output-path/custom-src/SeedSimpleApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSimpleApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f79f10bf40eb..000000000000 --- a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSimpleApi.Core; - -namespace SeedSimpleApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/Core/StringEnumSerializer.cs deleted file mode 100644 index f57d66a0377b..000000000000 --- a/seed/csharp-sdk/simple-api/no-custom-config/src/SeedSimpleApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSimpleApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 9690b2c8924b..000000000000 --- a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSingleUrlEnvironmentDefault.Core; - -namespace SeedSingleUrlEnvironmentDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index ab41210fc225..000000000000 --- a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSingleUrlEnvironmentDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 009dce166f4d..000000000000 --- a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSingleUrlEnvironmentNoDefault.Core; - -namespace SeedSingleUrlEnvironmentNoDefault.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs deleted file mode 100644 index f98cc99fd878..000000000000 --- a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSingleUrlEnvironmentNoDefault.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7301e5bb13d6..000000000000 --- a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedStreaming.Core; - -namespace SeedStreaming.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs deleted file mode 100644 index a70911174a2f..000000000000 --- a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedStreaming.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7301e5bb13d6..000000000000 --- a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedStreaming.Core; - -namespace SeedStreaming.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/Core/StringEnumSerializer.cs b/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/Core/StringEnumSerializer.cs deleted file mode 100644 index a70911174a2f..000000000000 --- a/seed/csharp-sdk/streaming/no-custom-config/src/SeedStreaming/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedStreaming.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 7301e5bb13d6..000000000000 --- a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedStreaming.Core; - -namespace SeedStreaming.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/Core/StringEnumSerializer.cs b/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/Core/StringEnumSerializer.cs deleted file mode 100644 index a70911174a2f..000000000000 --- a/seed/csharp-sdk/streaming/redact-response-body-on-error/src/SeedStreaming/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedStreaming.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a88e4c723a7b..000000000000 --- a/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedTrace.Core; - -namespace SeedTrace.Test_.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Commons/Types/Language.cs b/seed/csharp-sdk/trace/src/SeedTrace/Commons/Types/Language.cs index 1a4e792dfea8..1eb0bc178c93 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/Commons/Types/Language.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/Commons/Types/Language.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Language.LanguageSerializer))] [Serializable] public readonly record struct Language : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator Language(string value) => new(value); + internal class LanguageSerializer : JsonConverter + { + public override Language Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Language(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Language value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Core/StringEnumSerializer.cs b/seed/csharp-sdk/trace/src/SeedTrace/Core/StringEnumSerializer.cs deleted file mode 100644 index 9a93ba91f759..000000000000 --- a/seed/csharp-sdk/trace/src/SeedTrace/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedTrace.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Migration/Types/MigrationStatus.cs b/seed/csharp-sdk/trace/src/SeedTrace/Migration/Types/MigrationStatus.cs index d227b996d285..1f8b41f486d4 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/Migration/Types/MigrationStatus.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/Migration/Types/MigrationStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(MigrationStatus.MigrationStatusSerializer))] [Serializable] public readonly record struct MigrationStatus : IStringEnum { @@ -60,6 +61,32 @@ public override string ToString() public static explicit operator MigrationStatus(string value) => new(value); + internal class MigrationStatusSerializer : JsonConverter + { + public override MigrationStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new MigrationStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + MigrationStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Playlist/Types/ReservedKeywordEnum.cs b/seed/csharp-sdk/trace/src/SeedTrace/Playlist/Types/ReservedKeywordEnum.cs index bf8451443d48..436afb31a207 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/Playlist/Types/ReservedKeywordEnum.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/Playlist/Types/ReservedKeywordEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReservedKeywordEnum.ReservedKeywordEnumSerializer))] [Serializable] public readonly record struct ReservedKeywordEnum : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator ReservedKeywordEnum(string value) => new(value); + internal class ReservedKeywordEnumSerializer : JsonConverter + { + public override ReservedKeywordEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReservedKeywordEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReservedKeywordEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/ExecutionSessionStatus.cs b/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/ExecutionSessionStatus.cs index 0dc7eadf4116..2f713b6b4c90 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/ExecutionSessionStatus.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/ExecutionSessionStatus.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ExecutionSessionStatus.ExecutionSessionStatusSerializer))] [Serializable] public readonly record struct ExecutionSessionStatus : IStringEnum { @@ -62,6 +63,32 @@ public override string ToString() public static explicit operator ExecutionSessionStatus(string value) => new(value); + internal class ExecutionSessionStatusSerializer : JsonConverter + { + public override ExecutionSessionStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ExecutionSessionStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ExecutionSessionStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/RunningSubmissionState.cs b/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/RunningSubmissionState.cs index 208ca9dfbdf4..2ef80355165d 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/RunningSubmissionState.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/RunningSubmissionState.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(RunningSubmissionState.RunningSubmissionStateSerializer))] [Serializable] public readonly record struct RunningSubmissionState : IStringEnum { @@ -66,6 +67,32 @@ public override string ToString() public static explicit operator RunningSubmissionState(string value) => new(value); + internal class RunningSubmissionStateSerializer : JsonConverter + { + public override RunningSubmissionState Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new RunningSubmissionState(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + RunningSubmissionState value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/SubmissionTypeEnum.cs b/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/SubmissionTypeEnum.cs index 8fedafdbc8f3..34e2abf88591 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/SubmissionTypeEnum.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/Submission/Types/SubmissionTypeEnum.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedTrace.Core; namespace SeedTrace; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(SubmissionTypeEnum.SubmissionTypeEnumSerializer))] [Serializable] public readonly record struct SubmissionTypeEnum : IStringEnum { @@ -50,6 +51,32 @@ public override string ToString() public static explicit operator SubmissionTypeEnum(string value) => new(value); + internal class SubmissionTypeEnumSerializer : JsonConverter + { + public override SubmissionTypeEnum Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SubmissionTypeEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SubmissionTypeEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 93cfc064b533..000000000000 --- a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUndiscriminatedUnionWithResponseProperty.Core; - -namespace SeedUndiscriminatedUnionWithResponseProperty.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs b/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs deleted file mode 100644 index 382b481d7a0e..000000000000 --- a/seed/csharp-sdk/undiscriminated-union-with-response-property/src/SeedUndiscriminatedUnionWithResponseProperty/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUndiscriminatedUnionWithResponseProperty.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a93332581101..000000000000 --- a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUndiscriminatedUnions.Core; - -namespace SeedUndiscriminatedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index 10fa4bd8e4eb..000000000000 --- a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUndiscriminatedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs index 8aeadd98a54a..11b45673a238 100644 --- a/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs +++ b/seed/csharp-sdk/undiscriminated-unions/no-custom-config/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedUndiscriminatedUnions.Core; namespace SeedUndiscriminatedUnions; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(KeyType.KeyTypeSerializer))] [Serializable] public readonly record struct KeyType : IStringEnum { @@ -55,6 +56,32 @@ public override string ToString() public static explicit operator KeyType(string value) => new(value); + internal class KeyTypeSerializer : JsonConverter + { + public override KeyType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new KeyType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + KeyType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value_); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index a93332581101..000000000000 --- a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUndiscriminatedUnions.Core; - -namespace SeedUndiscriminatedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index 10fa4bd8e4eb..000000000000 --- a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUndiscriminatedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs index 8aeadd98a54a..11b45673a238 100644 --- a/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs +++ b/seed/csharp-sdk/undiscriminated-unions/with-undiscriminated-unions/src/SeedUndiscriminatedUnions/Union/Types/KeyType.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedUndiscriminatedUnions.Core; namespace SeedUndiscriminatedUnions; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(KeyType.KeyTypeSerializer))] [Serializable] public readonly record struct KeyType : IStringEnum { @@ -55,6 +56,32 @@ public override string ToString() public static explicit operator KeyType(string value) => new(value); + internal class KeyTypeSerializer : JsonConverter + { + public override KeyType Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new KeyType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + KeyType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value_); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f820460467fe..000000000000 --- a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUnions.Core; - -namespace SeedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs b/seed/csharp-sdk/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index a0bfe7062381..000000000000 --- a/seed/csharp-sdk/unions-with-local-date/src/SeedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f820460467fe..000000000000 --- a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUnions.Core; - -namespace SeedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/Core/StringEnumSerializer.cs b/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index a0bfe7062381..000000000000 --- a/seed/csharp-sdk/unions/no-custom-config/src/SeedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f820460467fe..000000000000 --- a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUnions.Core; - -namespace SeedUnions.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/Core/StringEnumSerializer.cs b/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/Core/StringEnumSerializer.cs deleted file mode 100644 index a0bfe7062381..000000000000 --- a/seed/csharp-sdk/unions/no-discriminated-unions/src/SeedUnions/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUnions.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f768e44645d3..000000000000 --- a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedUnknownAsAny.Core; - -namespace SeedUnknownAsAny.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs deleted file mode 100644 index b76f029c8b43..000000000000 --- a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedUnknownAsAny.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/url-form-encoded/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/url-form-encoded/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index c8ed6b24ee47..000000000000 --- a/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedValidation.Core; - -namespace SeedValidation.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/validation/src/SeedValidation/Core/StringEnumSerializer.cs b/seed/csharp-sdk/validation/src/SeedValidation/Core/StringEnumSerializer.cs deleted file mode 100644 index 855d6f1459a1..000000000000 --- a/seed/csharp-sdk/validation/src/SeedValidation/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedValidation.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/validation/src/SeedValidation/Types/Shape.cs b/seed/csharp-sdk/validation/src/SeedValidation/Types/Shape.cs index 8532b678c37b..10d218ab08ea 100644 --- a/seed/csharp-sdk/validation/src/SeedValidation/Types/Shape.cs +++ b/seed/csharp-sdk/validation/src/SeedValidation/Types/Shape.cs @@ -1,9 +1,10 @@ +using System.Text.Json; using System.Text.Json.Serialization; using SeedValidation.Core; namespace SeedValidation; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Shape.ShapeSerializer))] [Serializable] public readonly record struct Shape : IStringEnum { @@ -52,6 +53,32 @@ public override string ToString() public static explicit operator Shape(string value) => new(value); + internal class ShapeSerializer : JsonConverter + { + public override Shape Read( + ref Utf8JsonReader reader, + System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Shape(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Shape value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 749a3596736c..000000000000 --- a/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedVariables.Core; - -namespace SeedVariables.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/variables/src/SeedVariables/Core/StringEnumSerializer.cs b/seed/csharp-sdk/variables/src/SeedVariables/Core/StringEnumSerializer.cs deleted file mode 100644 index 79966dd7c0df..000000000000 --- a/seed/csharp-sdk/variables/src/SeedVariables/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedVariables.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3e19afddc513..000000000000 --- a/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedVersion.Core; - -namespace SeedVersion.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs deleted file mode 100644 index 2fc86e4fe8e4..000000000000 --- a/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedVersion.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3e19afddc513..000000000000 --- a/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedVersion.Core; - -namespace SeedVersion.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/version/src/SeedVersion/Core/StringEnumSerializer.cs b/seed/csharp-sdk/version/src/SeedVersion/Core/StringEnumSerializer.cs deleted file mode 100644 index 2fc86e4fe8e4..000000000000 --- a/seed/csharp-sdk/version/src/SeedVersion/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedVersion.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index d1e6b8a417de..000000000000 --- a/seed/csharp-sdk/webhook-audience/src/SeedApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedApi.Core; - -namespace SeedApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs deleted file mode 100644 index c6d8c4ec5a00..000000000000 --- a/seed/csharp-sdk/webhook-audience/src/SeedApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 928efb0a1f1f..000000000000 --- a/seed/csharp-sdk/webhooks/src/SeedWebhooks.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebhooks.Core; - -namespace SeedWebhooks.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs b/seed/csharp-sdk/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs deleted file mode 100644 index e9b458d13315..000000000000 --- a/seed/csharp-sdk/webhooks/src/SeedWebhooks/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebhooks.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index e7dc182cc86b..000000000000 --- a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebsocketBearerAuth.Core; - -namespace SeedWebsocketBearerAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs b/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index cc91017243f0..000000000000 --- a/seed/csharp-sdk/websocket-bearer-auth/src/SeedWebsocketBearerAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebsocketBearerAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index 3d79105fa6cf..000000000000 --- a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebsocketAuth.Core; - -namespace SeedWebsocketAuth.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs b/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs deleted file mode 100644 index 7cf74e4e8397..000000000000 --- a/seed/csharp-sdk/websocket-inferred-auth/src/SeedWebsocketAuth/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebsocketAuth.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b43a235f4587..000000000000 --- a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebsocket.Core; - -namespace SeedWebsocket.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/Core/StringEnumSerializer.cs b/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/Core/StringEnumSerializer.cs deleted file mode 100644 index 7e0887f1b58c..000000000000 --- a/seed/csharp-sdk/websocket/no-custom-config/src/SeedWebsocket/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebsocket.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index b43a235f4587..000000000000 --- a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedWebsocket.Core; - -namespace SeedWebsocket.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/Core/StringEnumSerializer.cs b/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/Core/StringEnumSerializer.cs deleted file mode 100644 index 7e0887f1b58c..000000000000 --- a/seed/csharp-sdk/websocket/with-websockets/src/SeedWebsocket/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedWebsocket.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} From bc159e751b0f12a30fc688828d017c00af833e10 Mon Sep 17 00:00:00 2001 From: David Konigsberg <72822263+davidkonigsberg@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:35:54 -0400 Subject: [PATCH 11/29] feat(ci): Turn on stale-bot for PRs (#13340) * Update stale-bot.yml * Update stale-bot.yml * Update stale-bot.yml * add missing permissions --- .github/workflows/stale-bot.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 41d655327b2d..e2c5c47c4461 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -1,4 +1,4 @@ -name: "Cleanup stale PRs and Branches" +name: "Cleanup stale PRs, Branches, and Issues" on: schedule: - cron: "30 1 * * *" @@ -7,20 +7,23 @@ on: permissions: pull-requests: write contents: write + issues: write env: DO_NOT_TRACK: "1" jobs: - stale-prs: + stale-prs-and-issues: runs-on: ubuntu-latest steps: - uses: actions/stale@v10 with: + stale-issue-message: "This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 30 days." + close-issue-message: "This issue was closed because it has been inactive for 30 days after being marked stale." + days-before-issue-stale: 180 + days-before-issue-close: 30 stale-pr-message: "This PR is stale because it has been open 25 days with no activity. Remove stale label or comment or this will be closed in 5 days." close-pr-message: "This PR was closed because it has been inactive for 5 days after being marked stale." - days-before-issue-stale: -1 - days-before-issue-close: -1 days-before-pr-stale: 25 days-before-pr-close: 5 operations-per-run: 500 From 791d1a29752b711f6ffd8737de5f524bb969d91e Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:44:08 -0400 Subject: [PATCH 12/29] chore(csharp): update csharp-sdk seed (#13528) Co-authored-by: patrickthornton --- .../Core/Json/StringEnumSerializerTests.cs | 138 ------------------ .../Core/RawClientTests/RetriesTests.cs | 13 +- .../Unit/MockServer/BaseMockServerTest.cs | 7 +- .../Unit/MockServer/User/GetTest.cs | 1 + .../Core/StringEnumSerializer.cs | 25 ---- 5 files changed, 11 insertions(+), 173 deletions(-) delete mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs delete mode 100644 seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f79f10bf40eb..000000000000 --- a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using NUnit.Framework; -using SeedSimpleApi.Core; - -namespace SeedSimpleApi.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs index 0c9ef9701755..7ba4a06ea18d 100644 --- a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using NUnit.Framework; using SeedSimpleApi.Core; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs index d2feea66b1b3..f67e7ceda192 100644 --- a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/BaseMockServerTest.cs @@ -6,14 +6,13 @@ namespace SeedSimpleApi.Test.Unit.MockServer; -[SetUpFixture] public class BaseMockServerTest { - protected static WireMockServer Server { get; set; } = null!; + protected WireMockServer Server { get; set; } = null!; - protected static SeedSimpleApiClient Client { get; set; } = null!; + protected SeedSimpleApiClient Client { get; set; } = null!; - protected static RequestOptions RequestOptions { get; set; } = new(); + protected RequestOptions RequestOptions { get; set; } = new(); [OneTimeSetUp] public void GlobalSetup() diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs index 78425be96816..d81ed9d7487d 100644 --- a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs +++ b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi.Test/Unit/MockServer/User/GetTest.cs @@ -5,6 +5,7 @@ namespace SeedSimpleApi.Test.Unit.MockServer.User; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs b/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs deleted file mode 100644 index f57d66a0377b..000000000000 --- a/seed/csharp-sdk/simple-api/use-sln-format/src/SeedSimpleApi/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace SeedSimpleApi.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} From ac1f89e44fcb16066ab0804f401b92526cc433fb Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:45:32 -0400 Subject: [PATCH 13/29] chore(csharp): update csharp-model seed (#13529) Co-authored-by: patrickthornton Co-authored-by: David Konigsberg <72822263+davidkonigsberg@users.noreply.github.com> From 192bcafd24b306a5f424788de4901e2c6ef1cdfc Mon Sep 17 00:00:00 2001 From: patrick thornton <70873350+patrickthornton@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:46:28 -0400 Subject: [PATCH 14/29] feat(parser): `coerce-consts-to` coerces `const`s to either `enums` (default) or `literals` (#13525) coerce-consts-to --- .../openapi/openapi-ir-parser/src/options.ts | 11 +- .../src/schema/convertSchemas.ts | 26 +- .../openapi-ir-in-memory/anyOf.json | 3 +- .../application-json.json | 3 +- .../openapi-ir-in-memory/availability.json | 3 +- .../openapi-ir-in-memory/const.json | 3 +- .../openapi-ir-in-memory/dates.json | 3 +- .../inline-schema-reference.json | 3 +- .../preserve-single-schema-oneof.json | 3 +- .../openapi-ir-in-memory/url-reference.json | 3 +- .../openapi-ir/coerce-consts-to.json | 309 ++++++++++++++++++ .../openapi-ir/const-with-coerce-enums.json | 270 +++++++++++++++ .../openapi/coerce-consts-to.json | 145 ++++++++ .../openapi/const-with-coerce-enums.json | 169 ++++++++++ .../coerce-consts-to/fern/fern.config.json | 4 + .../coerce-consts-to/fern/generators.yml | 6 + .../fixtures/coerce-consts-to/openapi.yml | 61 ++++ .../fern/fern.config.json | 4 + .../fern/generators.yml | 7 + .../const-with-coerce-enums/openapi.yml | 57 ++++ .../src/api/adapter/LegacyApiSpecAdapter.ts | 6 +- .../migrator/converters/convertSettings.ts | 1 + packages/cli/cli/versions.yml | 24 ++ .../schemas/settings/BaseApiSettingsSchema.ts | 10 +- .../convertGeneratorsConfiguration.ts | 6 +- .../generators-yml/GeneratorsConfiguration.ts | 1 + .../generators/types/BaseApiSettingsSchema.ts | 7 + .../generators/types/CoerceConstsTo.ts | 8 + .../api/resources/generators/types/index.ts | 1 + .../generators/types/BaseApiSettingsSchema.ts | 3 + .../generators/types/CoerceConstsTo.ts | 14 + .../resources/generators/types/index.ts | 1 + .../src/OpenAPIWorkspace.ts | 6 +- .../lazy-fern-workspace/src/OSSWorkspace.ts | 6 +- .../src/openapi/BaseOpenAPIWorkspace.ts | 5 + .../src/openapi/getAPIDefinitionSettings.ts | 3 +- 36 files changed, 1174 insertions(+), 21 deletions(-) create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/coerce-consts-to.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/const-with-coerce-enums.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/coerce-consts-to.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const-with-coerce-enums.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/fern.config.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/generators.yml create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/openapi.yml create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/fern.config.json create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/generators.yml create mode 100644 packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/openapi.yml create mode 100644 packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts create mode 100644 packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts index 72738a954c2d..a762c956142c 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts @@ -116,6 +116,14 @@ export interface ParseOpenAPIOptions { * Defaults to false. */ inferForwardCompatible: boolean; + + /** + * Controls how `const` values in OpenAPI specs are represented. + * - `literals`: Convert const values directly to literals with defaults. + * - `enums`: Convert const values to single-element enums (current behavior). + * Defaults to `enums`. + */ + coerceConstsTo: "literals" | "enums"; } export const DEFAULT_PARSE_OPENAPI_SETTINGS: ParseOpenAPIOptions = { @@ -152,7 +160,8 @@ export const DEFAULT_PARSE_OPENAPI_SETTINGS: ParseOpenAPIOptions = { defaultIntegerFormat: generatorsYml.DefaultIntegerFormat.Int32, pathParameterOrder: generatorsYml.PathParameterOrder.UrlOrder, resolveSchemaCollisions: false, - inferForwardCompatible: false + inferForwardCompatible: false, + coerceConstsTo: "enums" }; function mergeOptions(params: { diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts index 46c269bdd239..37348f5cd71c 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts @@ -466,8 +466,29 @@ export function convertSchemaObject( // const // NOTE(patrickthornton): This is an attribute of OpenAPIV3_1.SchemaObject; // at some point we should probably migrate to that object altogether. - if ("const" in schema) { - schema.enum = [schema.const]; + const isFromConst = "const" in schema; + if (isFromConst) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `const` is an OpenAPI 3.1 attribute not in the V3 types + const constValue = (schema as Record).const; + if (context.options.coerceConstsTo === "literals") { + // Const directly becomes a literal — skip enum path entirely + if (typeof constValue === "string" || typeof constValue === "boolean") { + return convertLiteral({ + nameOverride, + generatedName, + title, + wrapAsOptional, + wrapAsNullable, + value: constValue, + description, + availability, + namespace, + groupName + }); + } + } + // Default: coerce to enum (current behavior) + schema.enum = [constValue]; } // enums @@ -506,6 +527,7 @@ export function convertSchemaObject( if ( context.options.coerceEnumsToLiterals && + !isFromConst && schema.enum.length === 1 && schema.enum[0] != null && fernEnum == null diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json index 0bf3f5304b1b..81eb75f603f1 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json @@ -105,6 +105,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json index 77d3146d1126..7259c30415c5 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json @@ -85,6 +85,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json index 24b0822a3930..e5cba73c4325 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json @@ -320,6 +320,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json index 3bc0ab743545..8cf49616cdde 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json @@ -119,6 +119,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json index 129f19e3b3c0..d10a41c4c3f5 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json @@ -129,6 +129,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json index 16255839ee69..b46edccd915f 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json @@ -117,6 +117,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json index b4e687d46f10..db0dba0cadda 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json @@ -84,6 +84,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json index 01ffe212dea8..a413d09b6885 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json @@ -72,6 +72,7 @@ "defaultIntegerFormat": "int32", "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, - "inferForwardCompatible": false + "inferForwardCompatible": false, + "coerceConstsTo": "enums" } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/coerce-consts-to.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/coerce-consts-to.json new file mode 100644 index 000000000000..74f60e6f4b88 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/coerce-consts-to.json @@ -0,0 +1,309 @@ +{ + "title": "Pet Store API", + "servers": [], + "websocketServers": [], + "tags": { + "tagsById": {} + }, + "hasEndpointsMarkedInternal": false, + "endpoints": [ + { + "summary": "Create a new pet", + "audiences": [], + "tags": [], + "pathParameters": [], + "queryParameters": [], + "headers": [], + "generatedRequestName": "PostPetsRequest", + "request": { + "schema": { + "generatedName": "PostPetsRequest", + "schema": "CreatePetRequest", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "contentType": "application/json", + "fullExamples": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "json" + }, + "response": { + "description": "Pet created successfully", + "schema": { + "generatedName": "PostPetsResponse", + "schema": "Pet", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "fullExamples": [], + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "statusCode": 201, + "type": "json" + }, + "errors": {}, + "servers": [], + "authed": false, + "method": "POST", + "path": "/pets", + "examples": [ + { + "pathParameters": [], + "queryParameters": [], + "headers": [], + "request": { + "properties": { + "type": { + "value": { + "value": "pet", + "type": "string" + }, + "type": "literal" + }, + "name": { + "value": { + "value": "Fluffy", + "type": "string" + }, + "type": "primitive" + } + }, + "type": "object" + }, + "response": { + "value": { + "properties": { + "id": { + "value": { + "value": 123, + "type": "int" + }, + "type": "primitive" + }, + "type": { + "value": { + "value": "pet", + "type": "string" + }, + "type": "literal" + }, + "name": { + "value": { + "value": "Fluffy", + "type": "string" + }, + "type": "primitive" + }, + "breed": { + "value": { + "value": "Golden Retriever", + "type": "string" + }, + "type": "primitive" + }, + "status": { + "value": { + "value": "active", + "type": "string" + }, + "type": "literal" + } + }, + "type": "object" + }, + "type": "withoutStreaming" + }, + "codeSamples": [], + "type": "full" + } + ], + "source": { + "file": "../openapi.yml", + "type": "openapi" + } + } + ], + "webhooks": [], + "channels": {}, + "groupedSchemas": { + "rootSchemas": { + "CreatePetRequest": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "createPetRequestType", + "key": "type", + "schema": { + "value": { + "value": "pet", + "type": "string" + }, + "generatedName": "CreatePetRequestType", + "groupName": [], + "type": "literal" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "createPetRequestName", + "key": "name", + "schema": { + "schema": { + "example": "Fluffy", + "type": "string" + }, + "generatedName": "CreatePetRequestName", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "createPetRequestBreed", + "key": "breed", + "schema": { + "generatedName": "CreatePetRequestBreed", + "value": { + "schema": { + "example": "Golden Retriever", + "type": "string" + }, + "generatedName": "CreatePetRequestBreed", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "CreatePetRequest", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + }, + "Pet": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "petId", + "key": "id", + "schema": { + "schema": { + "example": 123, + "type": "int" + }, + "generatedName": "PetId", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petType", + "key": "type", + "schema": { + "value": { + "value": "pet", + "type": "string" + }, + "generatedName": "PetType", + "groupName": [], + "type": "literal" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petName", + "key": "name", + "schema": { + "schema": { + "example": "Fluffy", + "type": "string" + }, + "generatedName": "PetName", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petBreed", + "key": "breed", + "schema": { + "generatedName": "PetBreed", + "value": { + "schema": { + "example": "Golden Retriever", + "type": "string" + }, + "generatedName": "PetBreed", + "groupName": [], + "type": "primitive" + }, + "groupName": [], + "type": "optional" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petStatus", + "key": "status", + "schema": { + "value": { + "value": "active", + "type": "string" + }, + "generatedName": "PetStatus", + "groupName": [], + "type": "literal" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "Pet", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + } + }, + "namespacedSchemas": {} + }, + "variables": {}, + "nonRequestReferencedSchemas": {}, + "securitySchemes": {}, + "globalHeaders": [], + "idempotencyHeaders": [], + "groups": {} +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/const-with-coerce-enums.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/const-with-coerce-enums.json new file mode 100644 index 000000000000..d93304136880 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/const-with-coerce-enums.json @@ -0,0 +1,270 @@ +{ + "title": "Pet Store API", + "servers": [], + "websocketServers": [], + "tags": { + "tagsById": {} + }, + "hasEndpointsMarkedInternal": false, + "endpoints": [ + { + "summary": "Create a new pet", + "audiences": [], + "tags": [], + "pathParameters": [], + "queryParameters": [], + "headers": [], + "generatedRequestName": "PostPetsRequest", + "request": { + "schema": { + "generatedName": "PostPetsRequest", + "schema": "CreatePetRequest", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "contentType": "application/json", + "fullExamples": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "json" + }, + "response": { + "description": "Pet created successfully", + "schema": { + "generatedName": "PostPetsResponse", + "schema": "Pet", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "fullExamples": [], + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "statusCode": 201, + "type": "json" + }, + "errors": {}, + "servers": [], + "authed": false, + "method": "POST", + "path": "/pets", + "examples": [ + { + "pathParameters": [], + "queryParameters": [], + "headers": [], + "request": { + "properties": { + "type": { + "value": "pet", + "type": "enum" + }, + "name": { + "value": { + "value": "Fluffy", + "type": "string" + }, + "type": "primitive" + } + }, + "type": "object" + }, + "response": { + "value": { + "properties": { + "id": { + "value": { + "value": 123, + "type": "int" + }, + "type": "primitive" + }, + "type": { + "value": "pet", + "type": "enum" + }, + "name": { + "value": { + "value": "Fluffy", + "type": "string" + }, + "type": "primitive" + }, + "status": { + "value": { + "value": "active", + "type": "string" + }, + "type": "literal" + } + }, + "type": "object" + }, + "type": "withoutStreaming" + }, + "codeSamples": [], + "type": "full" + } + ], + "source": { + "file": "../openapi.yml", + "type": "openapi" + } + } + ], + "webhooks": [], + "channels": {}, + "groupedSchemas": { + "rootSchemas": { + "CreatePetRequest": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "createPetRequestType", + "key": "type", + "schema": { + "generatedName": "CreatePetRequestType", + "values": [ + { + "generatedName": "pet", + "value": "pet", + "casing": {} + } + ], + "groupName": [], + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "enum" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "createPetRequestName", + "key": "name", + "schema": { + "schema": { + "example": "Fluffy", + "type": "string" + }, + "generatedName": "CreatePetRequestName", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "CreatePetRequest", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + }, + "Pet": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "petId", + "key": "id", + "schema": { + "schema": { + "example": 123, + "type": "int" + }, + "generatedName": "PetId", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petType", + "key": "type", + "schema": { + "generatedName": "PetType", + "values": [ + { + "generatedName": "pet", + "value": "pet", + "casing": {} + } + ], + "groupName": [], + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "enum" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petName", + "key": "name", + "schema": { + "schema": { + "example": "Fluffy", + "type": "string" + }, + "generatedName": "PetName", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petStatus", + "key": "status", + "schema": { + "value": { + "value": "active", + "type": "string" + }, + "generatedName": "PetStatus", + "groupName": [], + "type": "literal" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "Pet", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + } + }, + "namespacedSchemas": {} + }, + "variables": {}, + "nonRequestReferencedSchemas": {}, + "securitySchemes": {}, + "globalHeaders": [], + "idempotencyHeaders": [], + "groups": {} +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/coerce-consts-to.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/coerce-consts-to.json new file mode 100644 index 000000000000..9b925c09da79 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/coerce-consts-to.json @@ -0,0 +1,145 @@ +{ + "absoluteFilePath": "/DUMMY_PATH", + "importedDefinitions": {}, + "namedDefinitionFiles": { + "__package__.yml": { + "absoluteFilepath": "/DUMMY_PATH", + "contents": { + "service": { + "auth": false, + "base-path": "", + "endpoints": { + "createANewPet": { + "auth": undefined, + "display-name": "Create a new pet", + "docs": undefined, + "examples": [ + { + "request": { + "name": "Fluffy", + "type": "pet", + }, + "response": { + "body": { + "breed": "Golden Retriever", + "id": 123, + "name": "Fluffy", + "status": "active", + "type": "pet", + }, + }, + }, + ], + "method": "POST", + "pagination": undefined, + "path": "/pets", + "request": { + "body": { + "properties": { + "breed": "optional", + "name": "string", + "type": "literal<"pet">", + }, + }, + "content-type": "application/json", + "headers": undefined, + "name": "CreatePetRequest", + "path-parameters": undefined, + "query-parameters": undefined, + }, + "response": { + "docs": "Pet created successfully", + "status-code": 201, + "type": "Pet", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + "types": { + "Pet": { + "docs": undefined, + "inline": undefined, + "properties": { + "breed": "optional", + "id": "integer", + "name": "string", + "status": "literal<"active">", + "type": "literal<"pet">", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + }, + }, + "rawContents": "service: + auth: false + base-path: '' + endpoints: + createANewPet: + path: /pets + method: POST + source: + openapi: ../openapi.yml + display-name: Create a new pet + request: + name: CreatePetRequest + body: + properties: + type: literal<"pet"> + name: string + breed: optional + content-type: application/json + response: + docs: Pet created successfully + type: Pet + status-code: 201 + examples: + - request: + type: pet + name: Fluffy + response: + body: + id: 123 + type: pet + name: Fluffy + breed: Golden Retriever + status: active + source: + openapi: ../openapi.yml +types: + Pet: + properties: + id: integer + type: literal<"pet"> + name: string + breed: optional + status: literal<"active"> + source: + openapi: ../openapi.yml +", + }, + }, + "packageMarkers": {}, + "rootApiFile": { + "contents": { + "display-name": "Pet Store API", + "error-discrimination": { + "strategy": "status-code", + }, + "name": "api", + }, + "defaultUrl": undefined, + "rawContents": "name: api +error-discrimination: + strategy: status-code +display-name: Pet Store API +", + }, +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const-with-coerce-enums.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const-with-coerce-enums.json new file mode 100644 index 000000000000..019e48da0628 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const-with-coerce-enums.json @@ -0,0 +1,169 @@ +{ + "absoluteFilePath": "/DUMMY_PATH", + "importedDefinitions": {}, + "namedDefinitionFiles": { + "__package__.yml": { + "absoluteFilepath": "/DUMMY_PATH", + "contents": { + "service": { + "auth": false, + "base-path": "", + "endpoints": { + "createANewPet": { + "auth": undefined, + "display-name": "Create a new pet", + "docs": undefined, + "examples": [ + { + "request": { + "name": "Fluffy", + "type": "pet", + }, + "response": { + "body": { + "id": 123, + "name": "Fluffy", + "status": "active", + "type": "pet", + }, + }, + }, + ], + "method": "POST", + "pagination": undefined, + "path": "/pets", + "request": { + "body": { + "properties": { + "name": "string", + "type": "CreatePetRequestType", + }, + }, + "content-type": "application/json", + "headers": undefined, + "name": "CreatePetRequest", + "path-parameters": undefined, + "query-parameters": undefined, + }, + "response": { + "docs": "Pet created successfully", + "status-code": 201, + "type": "Pet", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + "types": { + "CreatePetRequestType": { + "enum": [ + "pet", + ], + "inline": true, + "source": { + "openapi": "../openapi.yml", + }, + }, + "Pet": { + "docs": undefined, + "inline": undefined, + "properties": { + "id": "integer", + "name": "string", + "status": "literal<"active">", + "type": "PetType", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + "PetType": { + "enum": [ + "pet", + ], + "inline": true, + "source": { + "openapi": "../openapi.yml", + }, + }, + }, + }, + "rawContents": "types: + CreatePetRequestType: + enum: + - pet + inline: true + source: + openapi: ../openapi.yml + PetType: + enum: + - pet + inline: true + source: + openapi: ../openapi.yml + Pet: + properties: + id: integer + type: PetType + name: string + status: literal<"active"> + source: + openapi: ../openapi.yml +service: + auth: false + base-path: '' + endpoints: + createANewPet: + path: /pets + method: POST + source: + openapi: ../openapi.yml + display-name: Create a new pet + request: + name: CreatePetRequest + body: + properties: + type: CreatePetRequestType + name: string + content-type: application/json + response: + docs: Pet created successfully + type: Pet + status-code: 201 + examples: + - request: + type: pet + name: Fluffy + response: + body: + id: 123 + type: pet + name: Fluffy + status: active + source: + openapi: ../openapi.yml +", + }, + }, + "packageMarkers": {}, + "rootApiFile": { + "contents": { + "display-name": "Pet Store API", + "error-discrimination": { + "strategy": "status-code", + }, + "name": "api", + }, + "defaultUrl": undefined, + "rawContents": "name: api +error-discrimination: + strategy: status-code +display-name: Pet Store API +", + }, +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/fern.config.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/fern.config.json new file mode 100644 index 000000000000..ecb7133e2645 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "*" +} diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/generators.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/generators.yml new file mode 100644 index 000000000000..6a5562314c34 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/fern/generators.yml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +api: + specs: + - openapi: ../openapi.yml + settings: + coerce-consts-to: literals diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/openapi.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/openapi.yml new file mode 100644 index 000000000000..b54f5c0cfc90 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/coerce-consts-to/openapi.yml @@ -0,0 +1,61 @@ +openapi: 3.1.0 +info: + title: Pet Store API + version: 1.0.0 + +paths: + /pets: + post: + summary: Create a new pet + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePetRequest" + responses: + "201": + description: Pet created successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + +components: + schemas: + CreatePetRequest: + type: object + required: + - type + - name + properties: + type: + const: "pet" + name: + type: string + example: "Fluffy" + breed: + type: string + example: "Golden Retriever" + + Pet: + type: object + required: + - id + - type + - name + - status + properties: + id: + type: integer + example: 123 + type: + const: "pet" + name: + type: string + example: "Fluffy" + breed: + type: string + example: "Golden Retriever" + status: + const: "active" diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/fern.config.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/fern.config.json new file mode 100644 index 000000000000..ecb7133e2645 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "*" +} diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/generators.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/generators.yml new file mode 100644 index 000000000000..32d809cf7bf3 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/fern/generators.yml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +api: + specs: + - openapi: ../openapi.yml + settings: + coerce-enums-to-literals: true + coerce-consts-to: enums diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/openapi.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/openapi.yml new file mode 100644 index 000000000000..8932b15417ea --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums/openapi.yml @@ -0,0 +1,57 @@ +openapi: 3.1.0 +info: + title: Pet Store API + version: 1.0.0 + +paths: + /pets: + post: + summary: Create a new pet + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePetRequest" + responses: + "201": + description: Pet created successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + +components: + schemas: + CreatePetRequest: + type: object + required: + - type + - name + properties: + type: + const: "pet" + name: + type: string + example: "Fluffy" + + Pet: + type: object + required: + - id + - type + - name + - status + properties: + id: + type: integer + example: 123 + type: + const: "pet" + name: + type: string + example: "Fluffy" + status: + type: string + enum: + - active diff --git a/packages/cli/cli-v2/src/api/adapter/LegacyApiSpecAdapter.ts b/packages/cli/cli-v2/src/api/adapter/LegacyApiSpecAdapter.ts index c5246c7ae4da..0128b1ac41ef 100644 --- a/packages/cli/cli-v2/src/api/adapter/LegacyApiSpecAdapter.ts +++ b/packages/cli/cli-v2/src/api/adapter/LegacyApiSpecAdapter.ts @@ -147,7 +147,8 @@ export class LegacyApiSpecAdapter { inlineAllOfSchemas: settings.inlineAllOfSchemas, resolveAliases: settings.resolveAliases, groupMultiApiEnvironments: settings.groupMultiApiEnvironments, - defaultIntegerFormat: this.adaptDefaultIntegerFormat(settings.defaultIntegerFormat) + defaultIntegerFormat: this.adaptDefaultIntegerFormat(settings.defaultIntegerFormat), + coerceConstsTo: settings.coerceConstsTo }; const hasSettings = Object.values(result).some((v) => v != null); @@ -175,7 +176,8 @@ export class LegacyApiSpecAdapter { pathParameterOrder: this.adaptPathParameterOrder(settings.pathParameterOrder), // AsyncAPI-specific settings - asyncApiNaming: settings.messageNaming + asyncApiNaming: settings.messageNaming, + coerceConstsTo: settings.coerceConstsTo }; const hasSettings = Object.values(result).some((v) => v != null); diff --git a/packages/cli/cli-v2/src/migrator/converters/convertSettings.ts b/packages/cli/cli-v2/src/migrator/converters/convertSettings.ts index 63a610af4c49..d0fca5ab359f 100644 --- a/packages/cli/cli-v2/src/migrator/converters/convertSettings.ts +++ b/packages/cli/cli-v2/src/migrator/converters/convertSettings.ts @@ -17,6 +17,7 @@ const SETTINGS_KEY_MAP: Record = { "group-environments-by-host": "groupEnvironmentsByHost", "remove-discriminants-from-schemas": "removeDiscriminantsFromSchemas", "path-parameter-order": "pathParameterOrder", + "coerce-consts-to": "coerceConstsTo", // OpenAPI-specific settings "only-include-referenced-schemas": "onlyIncludeReferencedSchemas", diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index d106333d62fa..5508a070947a 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,28 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 4.28.0 + changelogEntry: + - summary: | + Add `coerce-consts-to` setting for controlling how OpenAPI/AsyncAPI + `const` values are represented. Set to `literals` to convert them directly + to literals with defaults; set to `enums` (the default, current behavior) + to convert them to single-valued and therefore extensible enums. + + When `coerce-consts-to` is `enums`, the `coerce-enums-to-literals` setting + will not transitively apply to const-originated enums, keeping the two + concepts independent. + + Usage: + ```yaml + api: + specs: + - asyncapi: ./asyncapi.yaml + settings: + coerce-consts-to: literals + ``` + type: feat + createdAt: "2026-03-13" + irVersion: 65 + - version: 4.27.0 changelogEntry: - summary: | diff --git a/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts b/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts index 31423ee30be2..ac963c1142c7 100644 --- a/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts +++ b/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts @@ -60,7 +60,15 @@ export const BaseApiSettingsSchema = z.object({ * - `specOrder`: Use the order path parameters are defined in the spec * Defaults to `urlOrder`. */ - pathParameterOrder: PathParameterOrderSchema.optional() + pathParameterOrder: PathParameterOrderSchema.optional(), + + /** + * Controls how `const` values in OpenAPI specs are represented. + * - `literals`: Convert const values directly to literals with defaults. + * - `enums`: Convert const values to single-element enums (current behavior). + * Defaults to `enums`. + */ + coerceConstsTo: z.enum(["literals", "enums"]).optional() }); export type BaseApiSettingsSchema = z.infer; diff --git a/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts b/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts index 373de9c9f7bc..777fe02e442f 100644 --- a/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts +++ b/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts @@ -49,7 +49,8 @@ const UNDEFINED_API_DEFINITION_SETTINGS: generatorsYml.APIDefinitionSettings = { pathParameterOrder: undefined, defaultIntegerFormat: undefined, resolveSchemaCollisions: undefined, - inferForwardCompatible: undefined + inferForwardCompatible: undefined, + coerceConstsTo: undefined }; export async function convertGeneratorsConfiguration({ @@ -179,7 +180,8 @@ export function parseBaseApiDefinitionSettingsSchema( ), pathParameterOrder: settings?.["path-parameter-order"], resolveSchemaCollisions: settings?.["resolve-schema-collisions"], - inferForwardCompatible: settings?.["infer-forward-compatible"] + inferForwardCompatible: settings?.["infer-forward-compatible"], + coerceConstsTo: settings?.["coerce-consts-to"] }; } diff --git a/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts b/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts index 78641f0b1416..8d1566330289 100644 --- a/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts +++ b/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts @@ -94,6 +94,7 @@ export interface APIDefinitionSettings { defaultIntegerFormat: generatorsYml.DefaultIntegerFormat | undefined; resolveSchemaCollisions: boolean | undefined; inferForwardCompatible: boolean | undefined; + coerceConstsTo: "literals" | "enums" | undefined; } export interface APIDefinitionLocation { diff --git a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts index 18f737ac4787..0e474b580fc8 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts @@ -53,4 +53,11 @@ export interface BaseApiSettingsSchema { "resolve-schema-collisions"?: boolean; /** If true, infer forward-compatible enums from `oneOf: [enum, string]` or `anyOf: [enum, string]` patterns. Defaults to false. */ "infer-forward-compatible"?: boolean; + /** + * Controls how `const` values in OpenAPI specs are represented. + * - `literals`: Convert const values directly to literals with defaults. + * - `enums`: Convert const values to single-element enums (current behavior). + * Defaults to `enums`. + */ + "coerce-consts-to"?: GeneratorsYml.CoerceConstsTo; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts new file mode 100644 index 000000000000..9801a3fa5bc3 --- /dev/null +++ b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +/** Controls how const values in OpenAPI specs are represented. */ +export const CoerceConstsTo = { + Literals: "literals", + Enums: "enums", +} as const; +export type CoerceConstsTo = (typeof CoerceConstsTo)[keyof typeof CoerceConstsTo]; diff --git a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/index.ts b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/index.ts index f37eff575f95..32a489af79f9 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/index.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/index.ts @@ -11,6 +11,7 @@ export * from "./ApiDefinitionWithOverridesSchema.js"; export * from "./AsyncApiSettingsSchema.js"; export * from "./AsyncApiSpecSchema.js"; export * from "./BaseApiSettingsSchema.js"; +export * from "./CoerceConstsTo.js"; export * from "./ConjureSchema.js"; export * from "./DefaultIntegerFormat.js"; export * from "./ExampleStyle.js"; diff --git a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/BaseApiSettingsSchema.ts b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/BaseApiSettingsSchema.ts index 7a679c4ad432..4cdd073fa098 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/BaseApiSettingsSchema.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/BaseApiSettingsSchema.ts @@ -3,6 +3,7 @@ import type * as GeneratorsYml from "../../../../api/index.js"; import * as core from "../../../../core/index.js"; import type * as serializers from "../../../index.js"; +import { CoerceConstsTo } from "./CoerceConstsTo.js"; import { PathParameterOrder } from "./PathParameterOrder.js"; import { RemoveDiscriminantsFromSchemas } from "./RemoveDiscriminantsFromSchemas.js"; @@ -22,6 +23,7 @@ export const BaseApiSettingsSchema: core.serialization.ObjectSchema< "path-parameter-order": PathParameterOrder.optional(), "resolve-schema-collisions": core.serialization.boolean().optional(), "infer-forward-compatible": core.serialization.boolean().optional(), + "coerce-consts-to": CoerceConstsTo.optional(), }); export declare namespace BaseApiSettingsSchema { @@ -38,5 +40,6 @@ export declare namespace BaseApiSettingsSchema { "path-parameter-order"?: PathParameterOrder.Raw | null; "resolve-schema-collisions"?: boolean | null; "infer-forward-compatible"?: boolean | null; + "coerce-consts-to"?: CoerceConstsTo.Raw | null; } } diff --git a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts new file mode 100644 index 000000000000..11cf45779449 --- /dev/null +++ b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts @@ -0,0 +1,14 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as GeneratorsYml from "../../../../api/index.js"; +import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; + +export const CoerceConstsTo: core.serialization.Schema< + serializers.CoerceConstsTo.Raw, + GeneratorsYml.CoerceConstsTo +> = core.serialization.enum_(["literals", "enums"]); + +export declare namespace CoerceConstsTo { + export type Raw = "literals" | "enums"; +} diff --git a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/index.ts b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/index.ts index f37eff575f95..32a489af79f9 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/index.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/index.ts @@ -11,6 +11,7 @@ export * from "./ApiDefinitionWithOverridesSchema.js"; export * from "./AsyncApiSettingsSchema.js"; export * from "./AsyncApiSpecSchema.js"; export * from "./BaseApiSettingsSchema.js"; +export * from "./CoerceConstsTo.js"; export * from "./ConjureSchema.js"; export * from "./DefaultIntegerFormat.js"; export * from "./ExampleStyle.js"; diff --git a/packages/cli/workspace/browser-compatible-fern-workspace/src/OpenAPIWorkspace.ts b/packages/cli/workspace/browser-compatible-fern-workspace/src/OpenAPIWorkspace.ts index 51fa2e14b7d0..022860dcaf23 100644 --- a/packages/cli/workspace/browser-compatible-fern-workspace/src/OpenAPIWorkspace.ts +++ b/packages/cli/workspace/browser-compatible-fern-workspace/src/OpenAPIWorkspace.ts @@ -60,7 +60,8 @@ export class OpenAPIWorkspace extends BaseOpenAPIWorkspaceSync { groupEnvironmentsByHost: spec.settings?.groupEnvironmentsByHost, removeDiscriminantsFromSchemas: spec.settings?.removeDiscriminantsFromSchemas, defaultIntegerFormat: spec.settings?.defaultIntegerFormat, - pathParameterOrder: spec.settings?.pathParameterOrder + pathParameterOrder: spec.settings?.pathParameterOrder, + coerceConstsTo: spec.settings?.coerceConstsTo }); this.spec = spec; this.loader = new InMemoryOpenAPILoader(); @@ -74,7 +75,8 @@ export class OpenAPIWorkspace extends BaseOpenAPIWorkspaceSync { resolveAliases: this.resolveAliases, groupEnvironmentsByHost: this.groupEnvironmentsByHost, defaultIntegerFormat: this.defaultIntegerFormat, - pathParameterOrder: this.pathParameterOrder + pathParameterOrder: this.pathParameterOrder, + coerceConstsTo: this.coerceConstsTo }; } diff --git a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts index 29868e8eae3c..56bc2df3032e 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts @@ -140,7 +140,8 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { exampleGeneration: specs[0]?.settings?.exampleGeneration, groupEnvironmentsByHost: specs.some((spec) => spec.settings?.groupEnvironmentsByHost), defaultIntegerFormat: specs[0]?.settings?.defaultIntegerFormat, - pathParameterOrder: specs[0]?.settings?.pathParameterOrder + pathParameterOrder: specs[0]?.settings?.pathParameterOrder, + coerceConstsTo: specs[0]?.settings?.coerceConstsTo }); this.specs = specs; this.allSpecs = allSpecs; @@ -165,7 +166,8 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { groupMultiApiEnvironments: this.groupMultiApiEnvironments, groupEnvironmentsByHost: this.groupEnvironmentsByHost, defaultIntegerFormat: this.defaultIntegerFormat, - pathParameterOrder: this.pathParameterOrder + pathParameterOrder: this.pathParameterOrder, + coerceConstsTo: this.coerceConstsTo }; } diff --git a/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts b/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts index 057c5f4fcef7..c846b1c228e4 100644 --- a/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts +++ b/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts @@ -25,6 +25,7 @@ export declare namespace BaseOpenAPIWorkspace { removeDiscriminantsFromSchemas: generatorsYml.RemoveDiscriminantsFromSchemas | undefined; defaultIntegerFormat: generatorsYml.DefaultIntegerFormat | undefined; pathParameterOrder: generatorsYml.PathParameterOrder | undefined; + coerceConstsTo: "literals" | "enums" | undefined; } export type Settings = Partial; @@ -48,6 +49,7 @@ export abstract class BaseOpenAPIWorkspace extends AbstractAPIWorkspace = { defaultIntegerFormat: "defaultIntegerFormat", pathParameterOrder: "pathParameterOrder", resolveSchemaCollisions: "resolveSchemaCollisions", - inferForwardCompatible: "inferForwardCompatible" + inferForwardCompatible: "inferForwardCompatible", + coerceConstsTo: "coerceConstsTo" }; function setIfDefined( From 35aa1feef5f80aabd76f47c75fd065b5103c87f5 Mon Sep 17 00:00:00 2001 From: Kunal Dawar <35455566+developerkunal@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:16:39 +0530 Subject: [PATCH 15/29] fix(go): ensure deterministic ordering in generated requests.go and requests_test.go (#13339) * fix(go): ensure deterministic ordering in generated requests.go and requests_test.go Go maps randomize iteration order, causing non-deterministic type ordering in requests.go when exportAllRequestsAtRoot is enabled. This sorts map keys before iteration using slices.SortFunc/cmp.Compare, and adds exhaustive test fixtures with duplicate endpoint names to reproduce and prevent regression. * chore(go): regenerate IR test definitions for new exhaustive fixtures * fix(go): move duplicate-names tests to standalone go-deterministic-ordering fixture Move duplicate-names services out of the shared exhaustive fixture into a new go-deterministic-ordering fixture with the full exhaustive IR definition. Remove export-all-requests-at-root from exhaustive since the ordering test now lives in the dedicated fixture. This avoids breaking C# and other generators that share the exhaustive test definitions. * chore(go): add ir-to-jsonschema snapshots for go-deterministic-ordering fixture * small fixes --------- Co-authored-by: patrick thornton <70873350+patrickthornton@users.noreply.github.com> Co-authored-by: patrick thornton --- generators/go/internal/generator/generator.go | 52 +- generators/go/sdk/versions.yml | 11 + .../pagination_PaginatedResponse.json | 39 + .../type_endpoints/put_Error.json | 62 + .../type_endpoints/put_ErrorCategory.json | 9 + .../type_endpoints/put_ErrorCode.json | 17 + .../type_endpoints/put_PutResponse.json | 81 + ...e_general-errors_BadObjectRequestInfo.json | 13 + .../type_types/docs_ObjectWithDocs.json | 13 + .../type_types/enum_WeatherReport.json | 10 + .../type_types/object_DoubleOptional.json | 28 + .../object_NestedObjectWithOptionalField.json | 179 + .../object_NestedObjectWithRequiredField.json | 169 + .../object_ObjectWithDatetimeLikeString.json | 18 + .../type_types/object_ObjectWithMapOfMap.json | 19 + .../object_ObjectWithOptionalField.json | 152 + .../object_ObjectWithRequiredField.json | 13 + .../object_ObjectWithUnknownField.json | 20 + .../type_types/object_OptionalAlias.json | 11 + .../type_types/union_Animal.json | 51 + .../type_types/union_Cat.json | 17 + .../type_types/union_Dog.json | 17 + .../type_types/union_MixedType.json | 20 + .../go-deterministic-ordering.json | 16260 ++ .../go-deterministic-ordering.json | 114182 +++++++++++++++ .../.fern/metadata.json | 11 + .../.github/workflows/ci.yml | 58 + .../go-deterministic-ordering/README.md | 198 + .../client/client.go | 45 + .../client/client_test.go | 45 + .../core/api_error.go | 47 + .../go-deterministic-ordering/core/http.go | 15 + .../go-deterministic-ordering/core/page.go | 96 + .../core/request_option.go | 135 + .../dynamic-snippets/example0/snippet.go | 26 + .../dynamic-snippets/example1/snippet.go | 31 + .../dynamic-snippets/example10/snippet.go | 27 + .../dynamic-snippets/example11/snippet.go | 29 + .../dynamic-snippets/example12/snippet.go | 31 + .../dynamic-snippets/example13/snippet.go | 27 + .../dynamic-snippets/example14/snippet.go | 29 + .../dynamic-snippets/example15/snippet.go | 31 + .../dynamic-snippets/example16/snippet.go | 27 + .../dynamic-snippets/example17/snippet.go | 29 + .../dynamic-snippets/example18/snippet.go | 31 + .../dynamic-snippets/example19/snippet.go | 24 + .../dynamic-snippets/example2/snippet.go | 25 + .../dynamic-snippets/example20/snippet.go | 22 + .../dynamic-snippets/example21/snippet.go | 26 + .../dynamic-snippets/example22/snippet.go | 27 + .../dynamic-snippets/example23/snippet.go | 74 + .../dynamic-snippets/example24/snippet.go | 22 + .../dynamic-snippets/example25/snippet.go | 73 + .../dynamic-snippets/example26/snippet.go | 26 + .../dynamic-snippets/example27/snippet.go | 30 + .../dynamic-snippets/example28/snippet.go | 78 + .../dynamic-snippets/example29/snippet.go | 77 + .../dynamic-snippets/example3/snippet.go | 28 + .../dynamic-snippets/example30/snippet.go | 129 + .../dynamic-snippets/example31/snippet.go | 28 + .../dynamic-snippets/example32/snippet.go | 28 + .../dynamic-snippets/example33/snippet.go | 30 + .../dynamic-snippets/example34/snippet.go | 30 + .../dynamic-snippets/example35/snippet.go | 31 + .../dynamic-snippets/example36/snippet.go | 22 + .../dynamic-snippets/example37/snippet.go | 22 + .../dynamic-snippets/example38/snippet.go | 27 + .../dynamic-snippets/example39/snippet.go | 27 + .../dynamic-snippets/example4/snippet.go | 25 + .../dynamic-snippets/example40/snippet.go | 27 + .../dynamic-snippets/example41/snippet.go | 27 + .../dynamic-snippets/example42/snippet.go | 24 + .../dynamic-snippets/example43/snippet.go | 24 + .../dynamic-snippets/example44/snippet.go | 27 + .../dynamic-snippets/example45/snippet.go | 23 + .../dynamic-snippets/example46/snippet.go | 23 + .../dynamic-snippets/example47/snippet.go | 23 + .../dynamic-snippets/example48/snippet.go | 23 + .../dynamic-snippets/example49/snippet.go | 23 + .../dynamic-snippets/example5/snippet.go | 28 + .../dynamic-snippets/example50/snippet.go | 26 + .../dynamic-snippets/example51/snippet.go | 26 + .../dynamic-snippets/example52/snippet.go | 26 + .../dynamic-snippets/example53/snippet.go | 23 + .../dynamic-snippets/example54/snippet.go | 26 + .../dynamic-snippets/example55/snippet.go | 29 + .../dynamic-snippets/example56}/snippet.go | 0 .../dynamic-snippets/example57/snippet.go | 21 + .../dynamic-snippets/example58/snippet.go | 0 .../dynamic-snippets/example59/snippet.go | 21 + .../dynamic-snippets/example6/snippet.go | 28 + .../dynamic-snippets/example60/snippet.go | 77 + .../dynamic-snippets/example61/snippet.go | 77 + .../dynamic-snippets/example62/snippet.go | 25 + .../dynamic-snippets/example63/snippet.go | 25 + .../dynamic-snippets/example64/snippet.go | 21 + .../dynamic-snippets/example65/snippet.go | 21 + .../dynamic-snippets/example66/snippet.go | 28 + .../dynamic-snippets/example7/snippet.go | 26 + .../dynamic-snippets/example8/snippet.go | 73 + .../dynamic-snippets/example9/snippet.go | 73 + .../endpoints/client/client.go | 70 + .../endpoints/container/client.go | 161 + .../endpoints_container_test.go | 275 + .../endpoints/container/raw_client.go | 359 + .../endpoints/contenttype/client.go | 65 + .../endpoints_content_type_test.go | 205 + .../endpoints/contenttype/raw_client.go | 109 + .../endpoints/duplicatenamesa/client.go | 85 + .../endpoints_duplicate_names_a_test.go | 147 + .../endpoints/duplicatenamesa/raw_client.go | 166 + .../endpoints/duplicatenamesb/client.go | 85 + .../endpoints_duplicate_names_b_test.go | 147 + .../endpoints/duplicatenamesb/raw_client.go | 166 + .../endpoints/duplicatenamesc/client.go | 85 + .../endpoints_duplicate_names_c_test.go | 147 + .../endpoints/duplicatenamesc/raw_client.go | 166 + .../endpoints/enum/client.go | 49 + .../endpoints_enum_test.go | 86 + .../endpoints/enum/raw_client.go | 72 + .../endpoints/error_codes.go | 9 + .../endpoints/httpmethods/client.go | 117 + .../endpoints_http_methods_test.go | 231 + .../endpoints/httpmethods/raw_client.go | 248 + .../endpoints/object/client.go | 166 + .../endpoints_object_test.go | 519 + .../endpoints/object/raw_client.go | 363 + .../endpoints/pagination.go | 111 + .../endpoints/pagination/client.go | 95 + .../endpoints_pagination_test.go | 93 + .../endpoints/pagination/raw_client.go | 27 + .../endpoints/pagination_test.go | 236 + .../endpoints/params/client.go | 194 + .../endpoints_params_test.go | 259 + .../endpoints/params/raw_client.go | 440 + .../endpoints/primitive/client.go | 178 + .../endpoints_primitive_test.go | 252 + .../endpoints/primitive/raw_client.go | 401 + .../endpoints/put.go | 300 + .../endpoints/put/client.go | 50 + .../endpoints_put_test/endpoints_put_test.go | 88 + .../endpoints/put/raw_client.go | 75 + .../endpoints/put_test.go | 640 + .../endpoints/union/client.go | 49 + .../endpoints_union_test.go | 91 + .../endpoints/union/raw_client.go | 72 + .../endpoints/urls/client.go | 88 + .../endpoints_urls_test.go | 146 + .../endpoints/urls/raw_client.go | 186 + .../go-deterministic-ordering/error_codes.go | 16 + .../go-deterministic-ordering/errors.go | 31 + .../go-deterministic-ordering/file_param.go | 41 + .../general_errors.go | 94 + .../general_errors_test.go | 153 + seed/go-sdk/go-deterministic-ordering/go.mod | 16 + seed/go-sdk/go-deterministic-ordering/go.sum | 12 + .../inlinedrequests/client.go | 51 + .../inlined_requests_test.go | 139 + .../inlinedrequests/raw_client.go | 74 + .../internal/caller.go | 311 + .../internal/caller_test.go | 705 + .../internal/error_decoder.go | 64 + .../internal/error_decoder_test.go | 59 + .../internal/explicit_fields.go | 116 + .../internal/explicit_fields_test.go | 645 + .../internal/extra_properties.go | 141 + .../internal/extra_properties_test.go | 228 + .../internal/http.go | 71 + .../internal/pager.go | 121 + .../internal/pager_test.go | 171 + .../internal/query.go | 353 + .../internal/query_test.go | 395 + .../internal/retrier.go | 239 + .../internal/retrier_test.go | 352 + .../internal/stringer.go | 13 + .../internal/time.go | 165 + .../noauth/client.go | 49 + .../noauth/no_auth_test/no_auth_test.go | 87 + .../noauth/raw_client.go | 73 + .../noreqbody/client.go | 61 + .../no_req_body_test/no_req_body_test.go | 104 + .../noreqbody/raw_client.go | 109 + .../option/request_option.go | 80 + .../go-deterministic-ordering/pointer.go | 137 + .../go-deterministic-ordering/pointer_test.go | 211 + .../go-deterministic-ordering/reference.md | 3797 + .../go-deterministic-ordering/requests.go | 743 + .../requests_test.go | 1490 + .../reqwithheaders/client.go | 49 + .../reqwithheaders/raw_client.go | 71 + .../req_with_headers_test.go | 90 + .../go-deterministic-ordering/snippet.json | 697 + .../go-deterministic-ordering/types/docs.go | 158 + .../types/docs_test.go | 153 + .../go-deterministic-ordering/types/enum.go | 35 + .../types/enum_test.go | 51 + .../go-deterministic-ordering/types/errors.go | 146 + .../go-deterministic-ordering/types/object.go | 954 + .../types/object_test.go | 2193 + .../go-deterministic-ordering/types/union.go | 431 + .../types/union_test.go | 617 + .../wiremock/docker-compose.test.yml | 14 + .../wiremock/wiremock-mappings.json | 1 + seed/go-sdk/seed.yml | 5 + .../definition/api.yml | 4 + .../definition/endpoints/container.yml | 55 + .../definition/endpoints/content-type.yml | 19 + .../endpoints/duplicate-names-a.yml | 39 + .../endpoints/duplicate-names-b.yml | 39 + .../endpoints/duplicate-names-c.yml | 39 + .../definition/endpoints/enum.yml | 12 + .../definition/endpoints/http-methods.yml | 49 + .../definition/endpoints/object.yml | 94 + .../definition/endpoints/pagination.yml | 31 + .../definition/endpoints/params.yml | 110 + .../definition/endpoints/primitive.yml | 60 + .../definition/endpoints/put.yml | 45 + .../definition/endpoints/union.yml | 12 + .../definition/endpoints/urls.yml | 25 + .../definition/general-errors.yml | 9 + .../definition/inlined-requests.yml | 25 + .../definition/no-auth.yml | 20 + .../definition/no-req-body.yml | 16 + .../definition/req-with-headers.yml | 14 + .../definition/types/docs.yml | 70 + .../definition/types/enum.yml | 12 + .../definition/types/object.yml | 79 + .../definition/types/union.yml | 29 + .../go-deterministic-ordering/generators.yml | 15 + 229 files changed, 160971 insertions(+), 8 deletions(-) create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/pagination_PaginatedResponse.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_Error.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCategory.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCode.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_PutResponse.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_general-errors_BadObjectRequestInfo.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/docs_ObjectWithDocs.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/enum_WeatherReport.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_DoubleOptional.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithOptionalField.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithRequiredField.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithDatetimeLikeString.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithMapOfMap.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithOptionalField.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithRequiredField.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithUnknownField.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_OptionalAlias.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Animal.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Cat.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Dog.json create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_MixedType.json create mode 100644 packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json create mode 100644 packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json create mode 100644 seed/go-sdk/go-deterministic-ordering/.fern/metadata.json create mode 100644 seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml create mode 100644 seed/go-sdk/go-deterministic-ordering/README.md create mode 100644 seed/go-sdk/go-deterministic-ordering/client/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/client/client_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/core/api_error.go create mode 100644 seed/go-sdk/go-deterministic-ordering/core/http.go create mode 100644 seed/go-sdk/go-deterministic-ordering/core/page.go create mode 100644 seed/go-sdk/go-deterministic-ordering/core/request_option.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example0/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example1/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example10/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example11/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example12/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example13/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example14/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example15/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example16/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example17/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example18/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example19/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example2/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example20/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example21/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example22/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example23/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example24/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example25/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example26/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example27/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example28/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example29/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example3/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example30/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example31/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example32/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example33/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example34/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example35/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example36/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example37/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example38/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example39/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example4/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example40/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example41/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example42/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example43/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example44/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example45/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example46/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example47/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example48/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example49/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example5/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example50/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example51/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example52/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example53/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example54/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example55/snippet.go rename seed/go-sdk/{exhaustive/no-custom-config/dynamic-snippets/example58 => go-deterministic-ordering/dynamic-snippets/example56}/snippet.go (100%) create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example57/snippet.go rename seed/go-sdk/{exhaustive/omit-empty-request-wrappers => go-deterministic-ordering}/dynamic-snippets/example58/snippet.go (100%) create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example59/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example6/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example60/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example61/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example62/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example63/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example64/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example65/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example66/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example7/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example8/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example9/snippet.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/client/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/container/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/container/endpoints_container_test/endpoints_container_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/container/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/endpoints_content_type_test/endpoints_content_type_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/endpoints_duplicate_names_a_test/endpoints_duplicate_names_a_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/endpoints_duplicate_names_b_test/endpoints_duplicate_names_b_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/endpoints_duplicate_names_c_test/endpoints_duplicate_names_c_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/enum/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/enum/endpoints_enum_test/endpoints_enum_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/enum/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/error_codes.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/endpoints_http_methods_test/endpoints_http_methods_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/object/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/object/endpoints_object_test/endpoints_object_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/object/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/pagination.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/pagination/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/pagination/endpoints_pagination_test/endpoints_pagination_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/pagination/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/pagination_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/params/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/params/endpoints_params_test/endpoints_params_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/params/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/primitive/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/primitive/endpoints_primitive_test/endpoints_primitive_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/primitive/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/put.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/put/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/put/endpoints_put_test/endpoints_put_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/put/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/put_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/union/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/union/endpoints_union_test/endpoints_union_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/union/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/urls/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/urls/endpoints_urls_test/endpoints_urls_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/endpoints/urls/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/error_codes.go create mode 100644 seed/go-sdk/go-deterministic-ordering/errors.go create mode 100644 seed/go-sdk/go-deterministic-ordering/file_param.go create mode 100644 seed/go-sdk/go-deterministic-ordering/general_errors.go create mode 100644 seed/go-sdk/go-deterministic-ordering/general_errors_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/go.mod create mode 100644 seed/go-sdk/go-deterministic-ordering/go.sum create mode 100644 seed/go-sdk/go-deterministic-ordering/inlinedrequests/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/inlinedrequests/inlined_requests_test/inlined_requests_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/inlinedrequests/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/caller.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/caller_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/error_decoder.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/error_decoder_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/explicit_fields.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/explicit_fields_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/extra_properties.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/extra_properties_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/http.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/pager.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/pager_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/query.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/query_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/retrier.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/retrier_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/stringer.go create mode 100644 seed/go-sdk/go-deterministic-ordering/internal/time.go create mode 100644 seed/go-sdk/go-deterministic-ordering/noauth/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/noauth/no_auth_test/no_auth_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/noauth/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/noreqbody/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/noreqbody/no_req_body_test/no_req_body_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/noreqbody/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/option/request_option.go create mode 100644 seed/go-sdk/go-deterministic-ordering/pointer.go create mode 100644 seed/go-sdk/go-deterministic-ordering/pointer_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/reference.md create mode 100644 seed/go-sdk/go-deterministic-ordering/requests.go create mode 100644 seed/go-sdk/go-deterministic-ordering/requests_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/reqwithheaders/client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/reqwithheaders/raw_client.go create mode 100644 seed/go-sdk/go-deterministic-ordering/reqwithheaders/req_with_headers_test/req_with_headers_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/snippet.json create mode 100644 seed/go-sdk/go-deterministic-ordering/types/docs.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/docs_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/enum.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/enum_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/errors.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/object.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/object_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/union.go create mode 100644 seed/go-sdk/go-deterministic-ordering/types/union_test.go create mode 100644 seed/go-sdk/go-deterministic-ordering/wiremock/docker-compose.test.yml create mode 100644 seed/go-sdk/go-deterministic-ordering/wiremock/wiremock-mappings.json create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/api.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/container.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/content-type.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-a.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-b.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-c.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/enum.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/http-methods.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/object.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/pagination.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/params.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/primitive.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/put.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/union.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/urls.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/general-errors.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/inlined-requests.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/no-auth.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/no-req-body.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/req-with-headers.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/types/docs.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/types/enum.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/types/object.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/definition/types/union.yml create mode 100644 test-definitions/fern/apis/go-deterministic-ordering/generators.yml diff --git a/generators/go/internal/generator/generator.go b/generators/go/internal/generator/generator.go index 2f4c67cb5ae1..f0f9ff4cbcc4 100644 --- a/generators/go/internal/generator/generator.go +++ b/generators/go/internal/generator/generator.go @@ -1,12 +1,14 @@ package generator import ( + "cmp" _ "embed" "encoding/json" "fmt" "os" "path" "path/filepath" + "slices" "sort" "strings" @@ -71,7 +73,8 @@ type SubpackageToGenerate struct { // NewSubpackagesToGenerate returns a slice of subpackages to generate from the given IR. func NewSubpackagesToGenerate(ir *fernir.IntermediateRepresentation) []*SubpackageToGenerate { var subpackagesToGenerate []*SubpackageToGenerate - for _, irSubpackage := range ir.Subpackages { + for _, subpackageId := range sortedMapKeys(ir.Subpackages) { + irSubpackage := ir.Subpackages[subpackageId] originalFernFilepath := irSubpackage.FernFilepath if len(irSubpackage.Subpackages) > 0 && irSubpackage.FernFilepath.File != nil { // This represents a nested root package, so we need to deposit @@ -166,7 +169,8 @@ func (g *Generator) generateModelTypes(ir *fernir.IntermediateRepresentation, mo } files := make([]*File, 0, len(fileInfoToTypes)) var generatedRootClients []*GeneratedClient - for fileInfo, typesToGenerate := range fileInfoToTypes { + for _, fileInfo := range sortedFileInfoKeys(fileInfoToTypes) { + typesToGenerate := fileInfoToTypes[fileInfo] writer := newFileWriter( fileInfo.filename, fileInfo.packageName, @@ -310,7 +314,8 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( writer.WriteDocs(ir.RootPackage.Docs) files = append(files, writer.DocsFile()) } - for _, subpackage := range ir.Subpackages { + for _, subpackageId := range sortedMapKeys(ir.Subpackages) { + subpackage := ir.Subpackages[subpackageId] if subpackage.Docs == nil || len(*subpackage.Docs) == 0 { continue } @@ -617,7 +622,9 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( } files = append(files, clientTestFile) // Generate the error types, if any. - for fileInfo, irErrors := range fileInfoToErrors(rootPackageName, ir.Errors) { + errorsByFileInfo := fileInfoToErrors(rootPackageName, ir.Errors) + for _, fileInfo := range sortedFileInfoKeys(errorsByFileInfo) { + irErrors := errorsByFileInfo[fileInfo] writer := newFileWriter( fileInfo.filename, fileInfo.packageName, @@ -1721,7 +1728,8 @@ func fileInfoToTypes( ) (map[fileInfo][]*typeToGenerate, error) { result := make(map[fileInfo][]*typeToGenerate) - for _, irService := range irServices { + for _, serviceId := range sortedMapKeys(irServices) { + irService := irServices[serviceId] subpackageFileInfo := fileInfoForType(rootPackageName, irService.Name.FernFilepath) for _, irEndpoint := range irService.Endpoints { if shouldSkipRequestType(irEndpoint, irService.Headers, inlinePathParameters, inlineFileProperties, omitEmptyRequestWrappers) { @@ -1754,7 +1762,8 @@ func fileInfoToTypes( if irServiceTypeReferenceInfo == nil { // If the service type reference info isn't provided, default // to the file-per-type naming convention. - for _, irType := range irTypes { + for _, typeId := range sortedMapKeys(irTypes) { + irType := irTypes[typeId] fileInfo := fileInfoForType(rootPackageName, irType.Name.FernFilepath) result[fileInfo] = append( result[fileInfo], @@ -1790,7 +1799,8 @@ func fileInfoToTypes( }, ) } - for serviceId, typeIds := range irServiceTypeReferenceInfo.TypesReferencedOnlyByService { + for _, serviceId := range sortedMapKeys(irServiceTypeReferenceInfo.TypesReferencedOnlyByService) { + typeIds := irServiceTypeReferenceInfo.TypesReferencedOnlyByService[serviceId] if serviceId == "service_" { // The root service requires special handling. continue @@ -1863,7 +1873,8 @@ func fileInfoToErrors( irErrorDeclarations map[fernir.ErrorId]*fernir.ErrorDeclaration, ) map[fileInfo][]*fernir.ErrorDeclaration { result := make(map[fileInfo][]*fernir.ErrorDeclaration) - for _, irErrorDeclaration := range irErrorDeclarations { + for _, errorId := range sortedMapKeys(irErrorDeclarations) { + irErrorDeclaration := irErrorDeclarations[errorId] var elements []string for _, packageName := range irErrorDeclaration.Name.FernFilepath.PackagePath { elements = append(elements, strings.ToLower(packageName.CamelCase.SafeName)) @@ -1885,6 +1896,21 @@ func fileInfoToErrors( return result } +// sortedFileInfoKeys returns the keys of a map keyed by fileInfo, sorted by filename. +func sortedFileInfoKeys[V any](m map[fileInfo]V) []fileInfo { + keys := make([]fileInfo, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.SortFunc(keys, func(a, b fileInfo) int { + if c := cmp.Compare(a.filename, b.filename); c != 0 { + return c + } + return cmp.Compare(a.packageName, b.packageName) + }) + return keys +} + func stringSetToSortedSlice(set map[string]struct{}) []string { sorted := make([]string, 0, len(set)) for s := range set { @@ -2047,6 +2073,16 @@ var pointerFunctionNames = map[string]struct{}{ // TODO: Add support for BigInteger. } +// sortedMapKeys returns the keys of the given map in sorted order. +func sortedMapKeys[K ~string, V any](m map[K]V) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.SortFunc(keys, func(a, b K) int { return cmp.Compare(a, b) }) + return keys +} + // valueOf dereferences the given value, or returns the zero value if nil. func valueOf[T any](value *T) T { var result T diff --git a/generators/go/sdk/versions.yml b/generators/go/sdk/versions.yml index 0ede315700e6..858f3dbc32cd 100644 --- a/generators/go/sdk/versions.yml +++ b/generators/go/sdk/versions.yml @@ -1,4 +1,15 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 1.30.1 + changelogEntry: + - summary: | + Fix non-deterministic ordering in generated requests.go and requests_test.go. + Go maps randomize iteration order, causing type ordering to vary across runs + when exportAllRequestsAtRoot is enabled. Map keys are now sorted before + iteration to ensure deterministic output. + type: fix + createdAt: "2026-03-13" + irVersion: 61 + - version: 1.30.0 changelogEntry: - summary: | diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/pagination_PaginatedResponse.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/pagination_PaginatedResponse.json new file mode 100644 index 000000000000..951c80ce3dbe --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/pagination_PaginatedResponse.json @@ -0,0 +1,39 @@ +{ + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.object.ObjectWithRequiredField" + } + }, + "next": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "items" + ], + "additionalProperties": false, + "definitions": { + "types.object.ObjectWithRequiredField": { + "type": "object", + "properties": { + "string": { + "type": "string" + } + }, + "required": [ + "string" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_Error.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_Error.json new file mode 100644 index 000000000000..69b3d451031e --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_Error.json @@ -0,0 +1,62 @@ +{ + "type": "object", + "properties": { + "category": { + "$ref": "#/definitions/endpoints.put.ErrorCategory" + }, + "code": { + "$ref": "#/definitions/endpoints.put.ErrorCode" + }, + "detail": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "category", + "code" + ], + "additionalProperties": false, + "definitions": { + "endpoints.put.ErrorCategory": { + "type": "string", + "enum": [ + "API_ERROR", + "AUTHENTICATION_ERROR", + "INVALID_REQUEST_ERROR" + ] + }, + "endpoints.put.ErrorCode": { + "type": "string", + "enum": [ + "INTERNAL_SERVER_ERROR", + "UNAUTHORIZED", + "FORBIDDEN", + "BAD_REQUEST", + "CONFLICT", + "GONE", + "UNPROCESSABLE_ENTITY", + "NOT_IMPLEMENTED", + "BAD_GATEWAY", + "SERVICE_UNAVAILABLE", + "Unknown" + ] + } + } +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCategory.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCategory.json new file mode 100644 index 000000000000..1bbd9dcb448a --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCategory.json @@ -0,0 +1,9 @@ +{ + "type": "string", + "enum": [ + "API_ERROR", + "AUTHENTICATION_ERROR", + "INVALID_REQUEST_ERROR" + ], + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCode.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCode.json new file mode 100644 index 000000000000..33d1873a3536 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_ErrorCode.json @@ -0,0 +1,17 @@ +{ + "type": "string", + "enum": [ + "INTERNAL_SERVER_ERROR", + "UNAUTHORIZED", + "FORBIDDEN", + "BAD_REQUEST", + "CONFLICT", + "GONE", + "UNPROCESSABLE_ENTITY", + "NOT_IMPLEMENTED", + "BAD_GATEWAY", + "SERVICE_UNAVAILABLE", + "Unknown" + ], + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_PutResponse.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_PutResponse.json new file mode 100644 index 000000000000..b2ce699c183d --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_endpoints/put_PutResponse.json @@ -0,0 +1,81 @@ +{ + "type": "object", + "properties": { + "errors": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/endpoints.put.Error" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "endpoints.put.ErrorCategory": { + "type": "string", + "enum": [ + "API_ERROR", + "AUTHENTICATION_ERROR", + "INVALID_REQUEST_ERROR" + ] + }, + "endpoints.put.ErrorCode": { + "type": "string", + "enum": [ + "INTERNAL_SERVER_ERROR", + "UNAUTHORIZED", + "FORBIDDEN", + "BAD_REQUEST", + "CONFLICT", + "GONE", + "UNPROCESSABLE_ENTITY", + "NOT_IMPLEMENTED", + "BAD_GATEWAY", + "SERVICE_UNAVAILABLE", + "Unknown" + ] + }, + "endpoints.put.Error": { + "type": "object", + "properties": { + "category": { + "$ref": "#/definitions/endpoints.put.ErrorCategory" + }, + "code": { + "$ref": "#/definitions/endpoints.put.ErrorCode" + }, + "detail": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "field": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "category", + "code" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_general-errors_BadObjectRequestInfo.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_general-errors_BadObjectRequestInfo.json new file mode 100644 index 000000000000..f50ccac10d76 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_general-errors_BadObjectRequestInfo.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/docs_ObjectWithDocs.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/docs_ObjectWithDocs.json new file mode 100644 index 000000000000..89e2e0481a3a --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/docs_ObjectWithDocs.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "string": { + "type": "string" + } + }, + "required": [ + "string" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/enum_WeatherReport.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/enum_WeatherReport.json new file mode 100644 index 000000000000..247b41d0eb25 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/enum_WeatherReport.json @@ -0,0 +1,10 @@ +{ + "type": "string", + "enum": [ + "SUNNY", + "CLOUDY", + "RAINING", + "SNOWING" + ], + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_DoubleOptional.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_DoubleOptional.json new file mode 100644 index 000000000000..367401f9d368 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_DoubleOptional.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties": { + "optionalAlias": { + "oneOf": [ + { + "$ref": "#/definitions/types.object.OptionalAlias" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "types.object.OptionalAlias": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithOptionalField.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithOptionalField.json new file mode 100644 index 000000000000..84223507008b --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithOptionalField.json @@ -0,0 +1,179 @@ +{ + "type": "object", + "properties": { + "string": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "NestedObject": { + "oneOf": [ + { + "$ref": "#/definitions/types.object.ObjectWithOptionalField" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "types.object.ObjectWithOptionalField": { + "type": "object", + "properties": { + "string": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "integer": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "long": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "double": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "bool": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "datetime": { + "oneOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "date": { + "oneOf": [ + { + "type": "string", + "format": "date" + }, + { + "type": "null" + } + ] + }, + "uuid": { + "oneOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "base64": { + "oneOf": [ + { + "type": "string", + "contentEncoding": "base64" + }, + { + "type": "null" + } + ] + }, + "list": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "set": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + { + "type": "null" + } + ] + }, + "map": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "bigint": { + "oneOf": [ + { + "type": "string", + "pattern": "^-?[0-9]+$" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithRequiredField.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithRequiredField.json new file mode 100644 index 000000000000..395c9e398043 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_NestedObjectWithRequiredField.json @@ -0,0 +1,169 @@ +{ + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "NestedObject": { + "$ref": "#/definitions/types.object.ObjectWithOptionalField" + } + }, + "required": [ + "string", + "NestedObject" + ], + "additionalProperties": false, + "definitions": { + "types.object.ObjectWithOptionalField": { + "type": "object", + "properties": { + "string": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "integer": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "long": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "double": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "bool": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "datetime": { + "oneOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "date": { + "oneOf": [ + { + "type": "string", + "format": "date" + }, + { + "type": "null" + } + ] + }, + "uuid": { + "oneOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "base64": { + "oneOf": [ + { + "type": "string", + "contentEncoding": "base64" + }, + { + "type": "null" + } + ] + }, + "list": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "set": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + { + "type": "null" + } + ] + }, + "map": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "bigint": { + "oneOf": [ + { + "type": "string", + "pattern": "^-?[0-9]+$" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithDatetimeLikeString.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithDatetimeLikeString.json new file mode 100644 index 000000000000..ecbc702756c9 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithDatetimeLikeString.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "datetimeLikeString": { + "type": "string" + }, + "actualDatetime": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "datetimeLikeString", + "actualDatetime" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithMapOfMap.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithMapOfMap.json new file mode 100644 index 000000000000..907017a0d033 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithMapOfMap.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "map": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "required": [ + "map" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithOptionalField.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithOptionalField.json new file mode 100644 index 000000000000..301139a3986b --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithOptionalField.json @@ -0,0 +1,152 @@ +{ + "type": "object", + "properties": { + "string": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "integer": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "long": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "double": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "bool": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "datetime": { + "oneOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "date": { + "oneOf": [ + { + "type": "string", + "format": "date" + }, + { + "type": "null" + } + ] + }, + "uuid": { + "oneOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "base64": { + "oneOf": [ + { + "type": "string", + "contentEncoding": "base64" + }, + { + "type": "null" + } + ] + }, + "list": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "set": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + { + "type": "null" + } + ] + }, + "map": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "bigint": { + "oneOf": [ + { + "type": "string", + "pattern": "^-?[0-9]+$" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithRequiredField.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithRequiredField.json new file mode 100644 index 000000000000..89e2e0481a3a --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithRequiredField.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "string": { + "type": "string" + } + }, + "required": [ + "string" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithUnknownField.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithUnknownField.json new file mode 100644 index 000000000000..e8a1fdd625c3 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_ObjectWithUnknownField.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "unknown": { + "type": [ + "string", + "number", + "boolean", + "object", + "array", + "null" + ] + } + }, + "required": [ + "unknown" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_OptionalAlias.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_OptionalAlias.json new file mode 100644 index 000000000000..97db51f9b409 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/object_OptionalAlias.json @@ -0,0 +1,11 @@ +{ + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Animal.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Animal.json new file mode 100644 index 000000000000..60f384b71aac --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Animal.json @@ -0,0 +1,51 @@ +{ + "type": "object", + "properties": { + "animal": { + "type": "string", + "enum": [ + "dog", + "cat" + ] + } + }, + "oneOf": [ + { + "properties": { + "animal": { + "const": "dog" + }, + "name": { + "type": "string" + }, + "likesToWoof": { + "type": "boolean" + } + }, + "required": [ + "animal", + "name", + "likesToWoof" + ] + }, + { + "properties": { + "animal": { + "const": "cat" + }, + "name": { + "type": "string" + }, + "likesToMeow": { + "type": "boolean" + } + }, + "required": [ + "animal", + "name", + "likesToMeow" + ] + } + ], + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Cat.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Cat.json new file mode 100644 index 000000000000..16bed6ba4391 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Cat.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "likesToMeow": { + "type": "boolean" + } + }, + "required": [ + "name", + "likesToMeow" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Dog.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Dog.json new file mode 100644 index 000000000000..e0d46ed0d8a1 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_Dog.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "likesToWoof": { + "type": "boolean" + } + }, + "required": [ + "name", + "likesToWoof" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_MixedType.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_MixedType.json new file mode 100644 index 000000000000..c7793c4249cb --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/go-deterministic-ordering/type_types/union_MixedType.json @@ -0,0 +1,20 @@ +{ + "anyOf": [ + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "definitions": {} +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json new file mode 100644 index 000000000000..3251382af5cd --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/go-deterministic-ordering.json @@ -0,0 +1,16260 @@ +{ + "version": "1.0.0", + "types": { + "type_endpoints/pagination:PaginatedResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "PaginatedResponse", + "camelCase": { + "unsafeName": "paginatedResponse", + "safeName": "paginatedResponse" + }, + "snakeCase": { + "unsafeName": "paginated_response", + "safeName": "paginated_response" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATED_RESPONSE", + "safeName": "PAGINATED_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PaginatedResponse", + "safeName": "PaginatedResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "items", + "camelCase": { + "unsafeName": "items", + "safeName": "items" + }, + "snakeCase": { + "unsafeName": "items", + "safeName": "items" + }, + "screamingSnakeCase": { + "unsafeName": "ITEMS", + "safeName": "ITEMS" + }, + "pascalCase": { + "unsafeName": "Items", + "safeName": "Items" + } + }, + "wireValue": "items" + }, + "typeReference": { + "type": "list", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "next", + "camelCase": { + "unsafeName": "next", + "safeName": "next" + }, + "snakeCase": { + "unsafeName": "next", + "safeName": "next" + }, + "screamingSnakeCase": { + "unsafeName": "NEXT", + "safeName": "NEXT" + }, + "pascalCase": { + "unsafeName": "Next", + "safeName": "Next" + } + }, + "wireValue": "next" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_endpoints/put:Error": { + "type": "object", + "declaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "category", + "camelCase": { + "unsafeName": "category", + "safeName": "category" + }, + "snakeCase": { + "unsafeName": "category", + "safeName": "category" + }, + "screamingSnakeCase": { + "unsafeName": "CATEGORY", + "safeName": "CATEGORY" + }, + "pascalCase": { + "unsafeName": "Category", + "safeName": "Category" + } + }, + "wireValue": "category" + }, + "typeReference": { + "type": "named", + "value": "type_endpoints/put:ErrorCategory" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "code", + "camelCase": { + "unsafeName": "code", + "safeName": "code" + }, + "snakeCase": { + "unsafeName": "code", + "safeName": "code" + }, + "screamingSnakeCase": { + "unsafeName": "CODE", + "safeName": "CODE" + }, + "pascalCase": { + "unsafeName": "Code", + "safeName": "Code" + } + }, + "wireValue": "code" + }, + "typeReference": { + "type": "named", + "value": "type_endpoints/put:ErrorCode" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "detail", + "camelCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "snakeCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "screamingSnakeCase": { + "unsafeName": "DETAIL", + "safeName": "DETAIL" + }, + "pascalCase": { + "unsafeName": "Detail", + "safeName": "Detail" + } + }, + "wireValue": "detail" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "field", + "camelCase": { + "unsafeName": "field", + "safeName": "field" + }, + "snakeCase": { + "unsafeName": "field", + "safeName": "field" + }, + "screamingSnakeCase": { + "unsafeName": "FIELD", + "safeName": "FIELD" + }, + "pascalCase": { + "unsafeName": "Field", + "safeName": "Field" + } + }, + "wireValue": "field" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_endpoints/put:ErrorCategory": { + "type": "enum", + "declaration": { + "name": { + "originalName": "ErrorCategory", + "camelCase": { + "unsafeName": "errorCategory", + "safeName": "errorCategory" + }, + "snakeCase": { + "unsafeName": "error_category", + "safeName": "error_category" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CATEGORY", + "safeName": "ERROR_CATEGORY" + }, + "pascalCase": { + "unsafeName": "ErrorCategory", + "safeName": "ErrorCategory" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "values": [ + { + "name": { + "originalName": "API_ERROR", + "camelCase": { + "unsafeName": "apiError", + "safeName": "apiError" + }, + "snakeCase": { + "unsafeName": "api_error", + "safeName": "api_error" + }, + "screamingSnakeCase": { + "unsafeName": "API_ERROR", + "safeName": "API_ERROR" + }, + "pascalCase": { + "unsafeName": "APIError", + "safeName": "APIError" + } + }, + "wireValue": "API_ERROR" + }, + { + "name": { + "originalName": "AUTHENTICATION_ERROR", + "camelCase": { + "unsafeName": "authenticationError", + "safeName": "authenticationError" + }, + "snakeCase": { + "unsafeName": "authentication_error", + "safeName": "authentication_error" + }, + "screamingSnakeCase": { + "unsafeName": "AUTHENTICATION_ERROR", + "safeName": "AUTHENTICATION_ERROR" + }, + "pascalCase": { + "unsafeName": "AuthenticationError", + "safeName": "AuthenticationError" + } + }, + "wireValue": "AUTHENTICATION_ERROR" + }, + { + "name": { + "originalName": "INVALID_REQUEST_ERROR", + "camelCase": { + "unsafeName": "invalidRequestError", + "safeName": "invalidRequestError" + }, + "snakeCase": { + "unsafeName": "invalid_request_error", + "safeName": "invalid_request_error" + }, + "screamingSnakeCase": { + "unsafeName": "INVALID_REQUEST_ERROR", + "safeName": "INVALID_REQUEST_ERROR" + }, + "pascalCase": { + "unsafeName": "InvalidRequestError", + "safeName": "InvalidRequestError" + } + }, + "wireValue": "INVALID_REQUEST_ERROR" + } + ] + }, + "type_endpoints/put:ErrorCode": { + "type": "enum", + "declaration": { + "name": { + "originalName": "ErrorCode", + "camelCase": { + "unsafeName": "errorCode", + "safeName": "errorCode" + }, + "snakeCase": { + "unsafeName": "error_code", + "safeName": "error_code" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CODE", + "safeName": "ERROR_CODE" + }, + "pascalCase": { + "unsafeName": "ErrorCode", + "safeName": "ErrorCode" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "values": [ + { + "name": { + "originalName": "INTERNAL_SERVER_ERROR", + "camelCase": { + "unsafeName": "internalServerError", + "safeName": "internalServerError" + }, + "snakeCase": { + "unsafeName": "internal_server_error", + "safeName": "internal_server_error" + }, + "screamingSnakeCase": { + "unsafeName": "INTERNAL_SERVER_ERROR", + "safeName": "INTERNAL_SERVER_ERROR" + }, + "pascalCase": { + "unsafeName": "InternalServerError", + "safeName": "InternalServerError" + } + }, + "wireValue": "INTERNAL_SERVER_ERROR" + }, + { + "name": { + "originalName": "UNAUTHORIZED", + "camelCase": { + "unsafeName": "unauthorized", + "safeName": "unauthorized" + }, + "snakeCase": { + "unsafeName": "unauthorized", + "safeName": "unauthorized" + }, + "screamingSnakeCase": { + "unsafeName": "UNAUTHORIZED", + "safeName": "UNAUTHORIZED" + }, + "pascalCase": { + "unsafeName": "Unauthorized", + "safeName": "Unauthorized" + } + }, + "wireValue": "UNAUTHORIZED" + }, + { + "name": { + "originalName": "FORBIDDEN", + "camelCase": { + "unsafeName": "forbidden", + "safeName": "forbidden" + }, + "snakeCase": { + "unsafeName": "forbidden", + "safeName": "forbidden" + }, + "screamingSnakeCase": { + "unsafeName": "FORBIDDEN", + "safeName": "FORBIDDEN" + }, + "pascalCase": { + "unsafeName": "Forbidden", + "safeName": "Forbidden" + } + }, + "wireValue": "FORBIDDEN" + }, + { + "name": { + "originalName": "BAD_REQUEST", + "camelCase": { + "unsafeName": "badRequest", + "safeName": "badRequest" + }, + "snakeCase": { + "unsafeName": "bad_request", + "safeName": "bad_request" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST", + "safeName": "BAD_REQUEST" + }, + "pascalCase": { + "unsafeName": "BadRequest", + "safeName": "BadRequest" + } + }, + "wireValue": "BAD_REQUEST" + }, + { + "name": { + "originalName": "CONFLICT", + "camelCase": { + "unsafeName": "conflict", + "safeName": "conflict" + }, + "snakeCase": { + "unsafeName": "conflict", + "safeName": "conflict" + }, + "screamingSnakeCase": { + "unsafeName": "CONFLICT", + "safeName": "CONFLICT" + }, + "pascalCase": { + "unsafeName": "Conflict", + "safeName": "Conflict" + } + }, + "wireValue": "CONFLICT" + }, + { + "name": { + "originalName": "GONE", + "camelCase": { + "unsafeName": "gone", + "safeName": "gone" + }, + "snakeCase": { + "unsafeName": "gone", + "safeName": "gone" + }, + "screamingSnakeCase": { + "unsafeName": "GONE", + "safeName": "GONE" + }, + "pascalCase": { + "unsafeName": "Gone", + "safeName": "Gone" + } + }, + "wireValue": "GONE" + }, + { + "name": { + "originalName": "UNPROCESSABLE_ENTITY", + "camelCase": { + "unsafeName": "unprocessableEntity", + "safeName": "unprocessableEntity" + }, + "snakeCase": { + "unsafeName": "unprocessable_entity", + "safeName": "unprocessable_entity" + }, + "screamingSnakeCase": { + "unsafeName": "UNPROCESSABLE_ENTITY", + "safeName": "UNPROCESSABLE_ENTITY" + }, + "pascalCase": { + "unsafeName": "UnprocessableEntity", + "safeName": "UnprocessableEntity" + } + }, + "wireValue": "UNPROCESSABLE_ENTITY" + }, + { + "name": { + "originalName": "NOT_IMPLEMENTED", + "camelCase": { + "unsafeName": "notImplemented", + "safeName": "notImplemented" + }, + "snakeCase": { + "unsafeName": "not_implemented", + "safeName": "not_implemented" + }, + "screamingSnakeCase": { + "unsafeName": "NOT_IMPLEMENTED", + "safeName": "NOT_IMPLEMENTED" + }, + "pascalCase": { + "unsafeName": "NotImplemented", + "safeName": "NotImplemented" + } + }, + "wireValue": "NOT_IMPLEMENTED" + }, + { + "name": { + "originalName": "BAD_GATEWAY", + "camelCase": { + "unsafeName": "badGateway", + "safeName": "badGateway" + }, + "snakeCase": { + "unsafeName": "bad_gateway", + "safeName": "bad_gateway" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_GATEWAY", + "safeName": "BAD_GATEWAY" + }, + "pascalCase": { + "unsafeName": "BadGateway", + "safeName": "BadGateway" + } + }, + "wireValue": "BAD_GATEWAY" + }, + { + "name": { + "originalName": "SERVICE_UNAVAILABLE", + "camelCase": { + "unsafeName": "serviceUnavailable", + "safeName": "serviceUnavailable" + }, + "snakeCase": { + "unsafeName": "service_unavailable", + "safeName": "service_unavailable" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE_UNAVAILABLE", + "safeName": "SERVICE_UNAVAILABLE" + }, + "pascalCase": { + "unsafeName": "ServiceUnavailable", + "safeName": "ServiceUnavailable" + } + }, + "wireValue": "SERVICE_UNAVAILABLE" + }, + { + "name": { + "originalName": "Unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "Unknown" + } + ] + }, + "type_endpoints/put:PutResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "PutResponse", + "camelCase": { + "unsafeName": "putResponse", + "safeName": "putResponse" + }, + "snakeCase": { + "unsafeName": "put_response", + "safeName": "put_response" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_RESPONSE", + "safeName": "PUT_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PutResponse", + "safeName": "PutResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "errors", + "camelCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "snakeCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "screamingSnakeCase": { + "unsafeName": "ERRORS", + "safeName": "ERRORS" + }, + "pascalCase": { + "unsafeName": "Errors", + "safeName": "Errors" + } + }, + "wireValue": "errors" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "list", + "value": { + "type": "named", + "value": "type_endpoints/put:Error" + } + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_general-errors:BadObjectRequestInfo": { + "type": "object", + "declaration": { + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "message", + "camelCase": { + "unsafeName": "message", + "safeName": "message" + }, + "snakeCase": { + "unsafeName": "message", + "safeName": "message" + }, + "screamingSnakeCase": { + "unsafeName": "MESSAGE", + "safeName": "MESSAGE" + }, + "pascalCase": { + "unsafeName": "Message", + "safeName": "Message" + } + }, + "wireValue": "message" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/docs:ObjectWithDocs": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithDocs", + "camelCase": { + "unsafeName": "objectWithDocs", + "safeName": "objectWithDocs" + }, + "snakeCase": { + "unsafeName": "object_with_docs", + "safeName": "object_with_docs" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DOCS", + "safeName": "OBJECT_WITH_DOCS" + }, + "pascalCase": { + "unsafeName": "ObjectWithDocs", + "safeName": "ObjectWithDocs" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/enum:WeatherReport": { + "type": "enum", + "declaration": { + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + } + }, + "values": [ + { + "name": { + "originalName": "SUNNY", + "camelCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "snakeCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "screamingSnakeCase": { + "unsafeName": "SUNNY", + "safeName": "SUNNY" + }, + "pascalCase": { + "unsafeName": "Sunny", + "safeName": "Sunny" + } + }, + "wireValue": "SUNNY" + }, + { + "name": { + "originalName": "CLOUDY", + "camelCase": { + "unsafeName": "cloudy", + "safeName": "cloudy" + }, + "snakeCase": { + "unsafeName": "cloudy", + "safeName": "cloudy" + }, + "screamingSnakeCase": { + "unsafeName": "CLOUDY", + "safeName": "CLOUDY" + }, + "pascalCase": { + "unsafeName": "Cloudy", + "safeName": "Cloudy" + } + }, + "wireValue": "CLOUDY" + }, + { + "name": { + "originalName": "RAINING", + "camelCase": { + "unsafeName": "raining", + "safeName": "raining" + }, + "snakeCase": { + "unsafeName": "raining", + "safeName": "raining" + }, + "screamingSnakeCase": { + "unsafeName": "RAINING", + "safeName": "RAINING" + }, + "pascalCase": { + "unsafeName": "Raining", + "safeName": "Raining" + } + }, + "wireValue": "RAINING" + }, + { + "name": { + "originalName": "SNOWING", + "camelCase": { + "unsafeName": "snowing", + "safeName": "snowing" + }, + "snakeCase": { + "unsafeName": "snowing", + "safeName": "snowing" + }, + "screamingSnakeCase": { + "unsafeName": "SNOWING", + "safeName": "SNOWING" + }, + "pascalCase": { + "unsafeName": "Snowing", + "safeName": "Snowing" + } + }, + "wireValue": "SNOWING" + } + ] + }, + "type_types/object:ObjectWithOptionalField": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "LONG" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "DOUBLE" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "DATE_TIME" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "DATE" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "UUID" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BASE_64" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "set", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "INTEGER" + }, + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BIG_INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:ObjectWithRequiredField": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:ObjectWithMapOfMap": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "typeReference": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:NestedObjectWithOptionalField": { + "type": "object", + "declaration": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:NestedObjectWithRequiredField": { + "type": "object", + "declaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "typeReference": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:DoubleOptional": { + "type": "object", + "declaration": { + "name": { + "originalName": "DoubleOptional", + "camelCase": { + "unsafeName": "doubleOptional", + "safeName": "doubleOptional" + }, + "snakeCase": { + "unsafeName": "double_optional", + "safeName": "double_optional" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE_OPTIONAL", + "safeName": "DOUBLE_OPTIONAL" + }, + "pascalCase": { + "unsafeName": "DoubleOptional", + "safeName": "DoubleOptional" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "optionalAlias", + "camelCase": { + "unsafeName": "optionalAlias", + "safeName": "optionalAlias" + }, + "snakeCase": { + "unsafeName": "optional_alias", + "safeName": "optional_alias" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_ALIAS", + "safeName": "OPTIONAL_ALIAS" + }, + "pascalCase": { + "unsafeName": "OptionalAlias", + "safeName": "OptionalAlias" + } + }, + "wireValue": "optionalAlias" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "named", + "value": "type_types/object:OptionalAlias" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:OptionalAlias": { + "type": "alias", + "declaration": { + "name": { + "originalName": "OptionalAlias", + "camelCase": { + "unsafeName": "optionalAlias", + "safeName": "optionalAlias" + }, + "snakeCase": { + "unsafeName": "optional_alias", + "safeName": "optional_alias" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_ALIAS", + "safeName": "OPTIONAL_ALIAS" + }, + "pascalCase": { + "unsafeName": "OptionalAlias", + "safeName": "OptionalAlias" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "type_types/object:ObjectWithDatetimeLikeString": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "datetimeLikeString", + "camelCase": { + "unsafeName": "datetimeLikeString", + "safeName": "datetimeLikeString" + }, + "snakeCase": { + "unsafeName": "datetime_like_string", + "safeName": "datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING", + "safeName": "DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeString", + "safeName": "DatetimeLikeString" + } + }, + "wireValue": "datetimeLikeString" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "actualDatetime", + "camelCase": { + "unsafeName": "actualDatetime", + "safeName": "actualDatetime" + }, + "snakeCase": { + "unsafeName": "actual_datetime", + "safeName": "actual_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "ACTUAL_DATETIME", + "safeName": "ACTUAL_DATETIME" + }, + "pascalCase": { + "unsafeName": "ActualDatetime", + "safeName": "ActualDatetime" + } + }, + "wireValue": "actualDatetime" + }, + "typeReference": { + "type": "primitive", + "value": "DATE_TIME" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:ObjectWithUnknownField": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "unknown" + }, + "typeReference": { + "type": "unknown" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/union:Animal": { + "type": "discriminatedUnion", + "declaration": { + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "discriminant": { + "name": { + "originalName": "animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "wireValue": "animal" + }, + "types": { + "dog": { + "type": "samePropertiesAsObject", + "typeId": "type_types/union:Dog", + "discriminantValue": { + "name": { + "originalName": "dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "wireValue": "dog" + }, + "properties": [] + }, + "cat": { + "type": "samePropertiesAsObject", + "typeId": "type_types/union:Cat", + "discriminantValue": { + "name": { + "originalName": "cat", + "camelCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "snakeCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "screamingSnakeCase": { + "unsafeName": "CAT", + "safeName": "CAT" + }, + "pascalCase": { + "unsafeName": "Cat", + "safeName": "Cat" + } + }, + "wireValue": "cat" + }, + "properties": [] + } + } + }, + "type_types/union:Dog": { + "type": "object", + "declaration": { + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "likesToWoof", + "camelCase": { + "unsafeName": "likesToWoof", + "safeName": "likesToWoof" + }, + "snakeCase": { + "unsafeName": "likes_to_woof", + "safeName": "likes_to_woof" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_WOOF", + "safeName": "LIKES_TO_WOOF" + }, + "pascalCase": { + "unsafeName": "LikesToWoof", + "safeName": "LikesToWoof" + } + }, + "wireValue": "likesToWoof" + }, + "typeReference": { + "type": "primitive", + "value": "BOOLEAN" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/union:Cat": { + "type": "object", + "declaration": { + "name": { + "originalName": "Cat", + "camelCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "snakeCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "screamingSnakeCase": { + "unsafeName": "CAT", + "safeName": "CAT" + }, + "pascalCase": { + "unsafeName": "Cat", + "safeName": "Cat" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "likesToMeow", + "camelCase": { + "unsafeName": "likesToMeow", + "safeName": "likesToMeow" + }, + "snakeCase": { + "unsafeName": "likes_to_meow", + "safeName": "likes_to_meow" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_MEOW", + "safeName": "LIKES_TO_MEOW" + }, + "pascalCase": { + "unsafeName": "LikesToMeow", + "safeName": "LikesToMeow" + } + }, + "wireValue": "likesToMeow" + }, + "typeReference": { + "type": "primitive", + "value": "BOOLEAN" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/union:MixedType": { + "type": "undiscriminatedUnion", + "declaration": { + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "types": [ + { + "type": "primitive", + "value": "DOUBLE" + }, + { + "type": "primitive", + "value": "BOOLEAN" + }, + { + "type": "primitive", + "value": "STRING" + }, + { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + } + ] + } + }, + "headers": [], + "endpoints": { + "endpoint_endpoints/container.getAndReturnListOfPrimitives": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnListOfPrimitives", + "camelCase": { + "unsafeName": "getAndReturnListOfPrimitives", + "safeName": "getAndReturnListOfPrimitives" + }, + "snakeCase": { + "unsafeName": "get_and_return_list_of_primitives", + "safeName": "get_and_return_list_of_primitives" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LIST_OF_PRIMITIVES", + "safeName": "GET_AND_RETURN_LIST_OF_PRIMITIVES" + }, + "pascalCase": { + "unsafeName": "GetAndReturnListOfPrimitives", + "safeName": "GetAndReturnListOfPrimitives" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/list-of-primitives" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnListOfObjects": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnListOfObjects", + "camelCase": { + "unsafeName": "getAndReturnListOfObjects", + "safeName": "getAndReturnListOfObjects" + }, + "snakeCase": { + "unsafeName": "get_and_return_list_of_objects", + "safeName": "get_and_return_list_of_objects" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LIST_OF_OBJECTS", + "safeName": "GET_AND_RETURN_LIST_OF_OBJECTS" + }, + "pascalCase": { + "unsafeName": "GetAndReturnListOfObjects", + "safeName": "GetAndReturnListOfObjects" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/list-of-objects" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "list", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnSetOfPrimitives": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnSetOfPrimitives", + "camelCase": { + "unsafeName": "getAndReturnSetOfPrimitives", + "safeName": "getAndReturnSetOfPrimitives" + }, + "snakeCase": { + "unsafeName": "get_and_return_set_of_primitives", + "safeName": "get_and_return_set_of_primitives" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_SET_OF_PRIMITIVES", + "safeName": "GET_AND_RETURN_SET_OF_PRIMITIVES" + }, + "pascalCase": { + "unsafeName": "GetAndReturnSetOfPrimitives", + "safeName": "GetAndReturnSetOfPrimitives" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/set-of-primitives" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "set", + "value": { + "type": "primitive", + "value": "STRING" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnSetOfObjects": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnSetOfObjects", + "camelCase": { + "unsafeName": "getAndReturnSetOfObjects", + "safeName": "getAndReturnSetOfObjects" + }, + "snakeCase": { + "unsafeName": "get_and_return_set_of_objects", + "safeName": "get_and_return_set_of_objects" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_SET_OF_OBJECTS", + "safeName": "GET_AND_RETURN_SET_OF_OBJECTS" + }, + "pascalCase": { + "unsafeName": "GetAndReturnSetOfObjects", + "safeName": "GetAndReturnSetOfObjects" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/set-of-objects" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "set", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnMapPrimToPrim": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnMapPrimToPrim", + "camelCase": { + "unsafeName": "getAndReturnMapPrimToPrim", + "safeName": "getAndReturnMapPrimToPrim" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_prim_to_prim", + "safeName": "get_and_return_map_prim_to_prim" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_PRIM_TO_PRIM", + "safeName": "GET_AND_RETURN_MAP_PRIM_TO_PRIM" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapPrimToPrim", + "safeName": "GetAndReturnMapPrimToPrim" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/map-prim-to-prim" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "primitive", + "value": "STRING" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnMapOfPrimToObject": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnMapOfPrimToObject", + "camelCase": { + "unsafeName": "getAndReturnMapOfPrimToObject", + "safeName": "getAndReturnMapOfPrimToObject" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_of_prim_to_object", + "safeName": "get_and_return_map_of_prim_to_object" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_OBJECT", + "safeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_OBJECT" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapOfPrimToObject", + "safeName": "GetAndReturnMapOfPrimToObject" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/map-prim-to-object" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnMapOfPrimToUndiscriminatedUnion": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnMapOfPrimToUndiscriminatedUnion", + "camelCase": { + "unsafeName": "getAndReturnMapOfPrimToUndiscriminatedUnion", + "safeName": "getAndReturnMapOfPrimToUndiscriminatedUnion" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_of_prim_to_undiscriminated_union", + "safeName": "get_and_return_map_of_prim_to_undiscriminated_union" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_UNDISCRIMINATED_UNION", + "safeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_UNDISCRIMINATED_UNION" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapOfPrimToUndiscriminatedUnion", + "safeName": "GetAndReturnMapOfPrimToUndiscriminatedUnion" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/map-prim-to-union" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "named", + "value": "type_types/union:MixedType" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnOptional": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnOptional", + "camelCase": { + "unsafeName": "getAndReturnOptional", + "safeName": "getAndReturnOptional" + }, + "snakeCase": { + "unsafeName": "get_and_return_optional", + "safeName": "get_and_return_optional" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_OPTIONAL", + "safeName": "GET_AND_RETURN_OPTIONAL" + }, + "pascalCase": { + "unsafeName": "GetAndReturnOptional", + "safeName": "GetAndReturnOptional" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/opt-objects" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "optional", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/content-type.postJsonPatchContentType": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postJsonPatchContentType", + "camelCase": { + "unsafeName": "postJSONPatchContentType", + "safeName": "postJSONPatchContentType" + }, + "snakeCase": { + "unsafeName": "post_json_patch_content_type", + "safeName": "post_json_patch_content_type" + }, + "screamingSnakeCase": { + "unsafeName": "POST_JSON_PATCH_CONTENT_TYPE", + "safeName": "POST_JSON_PATCH_CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "PostJSONPatchContentType", + "safeName": "PostJSONPatchContentType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + } + }, + "location": { + "method": "POST", + "path": "/foo/bar" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/content-type.postJsonPatchContentWithCharsetType": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postJsonPatchContentWithCharsetType", + "camelCase": { + "unsafeName": "postJSONPatchContentWithCharsetType", + "safeName": "postJSONPatchContentWithCharsetType" + }, + "snakeCase": { + "unsafeName": "post_json_patch_content_with_charset_type", + "safeName": "post_json_patch_content_with_charset_type" + }, + "screamingSnakeCase": { + "unsafeName": "POST_JSON_PATCH_CONTENT_WITH_CHARSET_TYPE", + "safeName": "POST_JSON_PATCH_CONTENT_WITH_CHARSET_TYPE" + }, + "pascalCase": { + "unsafeName": "PostJSONPatchContentWithCharsetType", + "safeName": "PostJSONPatchContentWithCharsetType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + } + }, + "location": { + "method": "POST", + "path": "/foo/baz" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-a.create": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "location": { + "method": "POST", + "path": "/duplicate-names-a" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateRequestA", + "camelCase": { + "unsafeName": "createRequestA", + "safeName": "createRequestA" + }, + "snakeCase": { + "unsafeName": "create_request_a", + "safeName": "create_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_A", + "safeName": "CREATE_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "CreateRequestA", + "safeName": "CreateRequestA" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "value", + "camelCase": { + "unsafeName": "value", + "safeName": "value" + }, + "snakeCase": { + "unsafeName": "value", + "safeName": "value" + }, + "screamingSnakeCase": { + "unsafeName": "VALUE", + "safeName": "VALUE" + }, + "pascalCase": { + "unsafeName": "Value", + "safeName": "Value" + } + }, + "wireValue": "value" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-a.get": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-a/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetRequestA", + "camelCase": { + "unsafeName": "getRequestA", + "safeName": "getRequestA" + }, + "snakeCase": { + "unsafeName": "get_request_a", + "safeName": "get_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_A", + "safeName": "GET_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "GetRequestA", + "safeName": "GetRequestA" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "filter", + "camelCase": { + "unsafeName": "filter", + "safeName": "filter" + }, + "snakeCase": { + "unsafeName": "filter", + "safeName": "filter" + }, + "screamingSnakeCase": { + "unsafeName": "FILTER", + "safeName": "FILTER" + }, + "pascalCase": { + "unsafeName": "Filter", + "safeName": "Filter" + } + }, + "wireValue": "filter" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-a.list": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-a" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListRequestA", + "camelCase": { + "unsafeName": "listRequestA", + "safeName": "listRequestA" + }, + "snakeCase": { + "unsafeName": "list_request_a", + "safeName": "list_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_A", + "safeName": "LIST_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "ListRequestA", + "safeName": "ListRequestA" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "page", + "camelCase": { + "unsafeName": "page", + "safeName": "page" + }, + "snakeCase": { + "unsafeName": "page", + "safeName": "page" + }, + "screamingSnakeCase": { + "unsafeName": "PAGE", + "safeName": "PAGE" + }, + "pascalCase": { + "unsafeName": "Page", + "safeName": "Page" + } + }, + "wireValue": "page" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-b.create": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "location": { + "method": "POST", + "path": "/duplicate-names-b" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateRequestB", + "camelCase": { + "unsafeName": "createRequestB", + "safeName": "createRequestB" + }, + "snakeCase": { + "unsafeName": "create_request_b", + "safeName": "create_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_B", + "safeName": "CREATE_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "CreateRequestB", + "safeName": "CreateRequestB" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "description", + "camelCase": { + "unsafeName": "description", + "safeName": "description" + }, + "snakeCase": { + "unsafeName": "description", + "safeName": "description" + }, + "screamingSnakeCase": { + "unsafeName": "DESCRIPTION", + "safeName": "DESCRIPTION" + }, + "pascalCase": { + "unsafeName": "Description", + "safeName": "Description" + } + }, + "wireValue": "description" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "count", + "camelCase": { + "unsafeName": "count", + "safeName": "count" + }, + "snakeCase": { + "unsafeName": "count", + "safeName": "count" + }, + "screamingSnakeCase": { + "unsafeName": "COUNT", + "safeName": "COUNT" + }, + "pascalCase": { + "unsafeName": "Count", + "safeName": "Count" + } + }, + "wireValue": "count" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-b.get": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-b/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetRequestB", + "camelCase": { + "unsafeName": "getRequestB", + "safeName": "getRequestB" + }, + "snakeCase": { + "unsafeName": "get_request_b", + "safeName": "get_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_B", + "safeName": "GET_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "GetRequestB", + "safeName": "GetRequestB" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "expand", + "camelCase": { + "unsafeName": "expand", + "safeName": "expand" + }, + "snakeCase": { + "unsafeName": "expand", + "safeName": "expand" + }, + "screamingSnakeCase": { + "unsafeName": "EXPAND", + "safeName": "EXPAND" + }, + "pascalCase": { + "unsafeName": "Expand", + "safeName": "Expand" + } + }, + "wireValue": "expand" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-b.list": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-b" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListRequestB", + "camelCase": { + "unsafeName": "listRequestB", + "safeName": "listRequestB" + }, + "snakeCase": { + "unsafeName": "list_request_b", + "safeName": "list_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_B", + "safeName": "LIST_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "ListRequestB", + "safeName": "ListRequestB" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "cursor", + "camelCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "snakeCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "screamingSnakeCase": { + "unsafeName": "CURSOR", + "safeName": "CURSOR" + }, + "pascalCase": { + "unsafeName": "Cursor", + "safeName": "Cursor" + } + }, + "wireValue": "cursor" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "size", + "camelCase": { + "unsafeName": "size", + "safeName": "size" + }, + "snakeCase": { + "unsafeName": "size", + "safeName": "size" + }, + "screamingSnakeCase": { + "unsafeName": "SIZE", + "safeName": "SIZE" + }, + "pascalCase": { + "unsafeName": "Size", + "safeName": "Size" + } + }, + "wireValue": "size" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-c.create": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "location": { + "method": "POST", + "path": "/duplicate-names-c" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateRequestC", + "camelCase": { + "unsafeName": "createRequestC", + "safeName": "createRequestC" + }, + "snakeCase": { + "unsafeName": "create_request_c", + "safeName": "create_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_C", + "safeName": "CREATE_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "CreateRequestC", + "safeName": "CreateRequestC" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "label", + "camelCase": { + "unsafeName": "label", + "safeName": "label" + }, + "snakeCase": { + "unsafeName": "label", + "safeName": "label" + }, + "screamingSnakeCase": { + "unsafeName": "LABEL", + "safeName": "LABEL" + }, + "pascalCase": { + "unsafeName": "Label", + "safeName": "Label" + } + }, + "wireValue": "label" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "priority", + "camelCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "snakeCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "screamingSnakeCase": { + "unsafeName": "PRIORITY", + "safeName": "PRIORITY" + }, + "pascalCase": { + "unsafeName": "Priority", + "safeName": "Priority" + } + }, + "wireValue": "priority" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-c.get": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-c/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetRequestC", + "camelCase": { + "unsafeName": "getRequestC", + "safeName": "getRequestC" + }, + "snakeCase": { + "unsafeName": "get_request_c", + "safeName": "get_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_C", + "safeName": "GET_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "GetRequestC", + "safeName": "GetRequestC" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "verbose", + "camelCase": { + "unsafeName": "verbose", + "safeName": "verbose" + }, + "snakeCase": { + "unsafeName": "verbose", + "safeName": "verbose" + }, + "screamingSnakeCase": { + "unsafeName": "VERBOSE", + "safeName": "VERBOSE" + }, + "pascalCase": { + "unsafeName": "Verbose", + "safeName": "Verbose" + } + }, + "wireValue": "verbose" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-c.list": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-c" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListRequestC", + "camelCase": { + "unsafeName": "listRequestC", + "safeName": "listRequestC" + }, + "snakeCase": { + "unsafeName": "list_request_c", + "safeName": "list_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_C", + "safeName": "LIST_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "ListRequestC", + "safeName": "ListRequestC" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "offset", + "camelCase": { + "unsafeName": "offset", + "safeName": "offset" + }, + "snakeCase": { + "unsafeName": "offset", + "safeName": "offset" + }, + "screamingSnakeCase": { + "unsafeName": "OFFSET", + "safeName": "OFFSET" + }, + "pascalCase": { + "unsafeName": "Offset", + "safeName": "Offset" + } + }, + "wireValue": "offset" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "count", + "camelCase": { + "unsafeName": "count", + "safeName": "count" + }, + "snakeCase": { + "unsafeName": "count", + "safeName": "count" + }, + "screamingSnakeCase": { + "unsafeName": "COUNT", + "safeName": "COUNT" + }, + "pascalCase": { + "unsafeName": "Count", + "safeName": "Count" + } + }, + "wireValue": "count" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/enum.getAndReturnEnum": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnEnum", + "camelCase": { + "unsafeName": "getAndReturnEnum", + "safeName": "getAndReturnEnum" + }, + "snakeCase": { + "unsafeName": "get_and_return_enum", + "safeName": "get_and_return_enum" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_ENUM", + "safeName": "GET_AND_RETURN_ENUM" + }, + "pascalCase": { + "unsafeName": "GetAndReturnEnum", + "safeName": "GetAndReturnEnum" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + } + }, + "location": { + "method": "POST", + "path": "/enum" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/enum:WeatherReport" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testGet": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testGet", + "camelCase": { + "unsafeName": "testGet", + "safeName": "testGet" + }, + "snakeCase": { + "unsafeName": "test_get", + "safeName": "test_get" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_GET", + "safeName": "TEST_GET" + }, + "pascalCase": { + "unsafeName": "TestGet", + "safeName": "TestGet" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "GET", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testPost": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testPost", + "camelCase": { + "unsafeName": "testPost", + "safeName": "testPost" + }, + "snakeCase": { + "unsafeName": "test_post", + "safeName": "test_post" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_POST", + "safeName": "TEST_POST" + }, + "pascalCase": { + "unsafeName": "TestPost", + "safeName": "TestPost" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "POST", + "path": "/http-methods" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testPut": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testPut", + "camelCase": { + "unsafeName": "testPut", + "safeName": "testPut" + }, + "snakeCase": { + "unsafeName": "test_put", + "safeName": "test_put" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_PUT", + "safeName": "TEST_PUT" + }, + "pascalCase": { + "unsafeName": "TestPut", + "safeName": "TestPut" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testPatch": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testPatch", + "camelCase": { + "unsafeName": "testPatch", + "safeName": "testPatch" + }, + "snakeCase": { + "unsafeName": "test_patch", + "safeName": "test_patch" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_PATCH", + "safeName": "TEST_PATCH" + }, + "pascalCase": { + "unsafeName": "TestPatch", + "safeName": "TestPatch" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "PATCH", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testDelete": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testDelete", + "camelCase": { + "unsafeName": "testDelete", + "safeName": "testDelete" + }, + "snakeCase": { + "unsafeName": "test_delete", + "safeName": "test_delete" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_DELETE", + "safeName": "TEST_DELETE" + }, + "pascalCase": { + "unsafeName": "TestDelete", + "safeName": "TestDelete" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "DELETE", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithOptionalField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithOptionalField", + "camelCase": { + "unsafeName": "getAndReturnWithOptionalField", + "safeName": "getAndReturnWithOptionalField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_optional_field", + "safeName": "get_and_return_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_OPTIONAL_FIELD", + "safeName": "GET_AND_RETURN_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithOptionalField", + "safeName": "GetAndReturnWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-optional-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithRequiredField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithRequiredField", + "camelCase": { + "unsafeName": "getAndReturnWithRequiredField", + "safeName": "getAndReturnWithRequiredField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_required_field", + "safeName": "get_and_return_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_REQUIRED_FIELD", + "safeName": "GET_AND_RETURN_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithRequiredField", + "safeName": "GetAndReturnWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-required-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithMapOfMap": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithMapOfMap", + "camelCase": { + "unsafeName": "getAndReturnWithMapOfMap", + "safeName": "getAndReturnWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_map_of_map", + "safeName": "get_and_return_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_MAP_OF_MAP", + "safeName": "GET_AND_RETURN_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithMapOfMap", + "safeName": "GetAndReturnWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-map-of-map" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithMapOfMap" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnNestedWithOptionalField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnNestedWithOptionalField", + "camelCase": { + "unsafeName": "getAndReturnNestedWithOptionalField", + "safeName": "getAndReturnNestedWithOptionalField" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_optional_field", + "safeName": "get_and_return_nested_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_OPTIONAL_FIELD", + "safeName": "GET_AND_RETURN_NESTED_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithOptionalField", + "safeName": "GetAndReturnNestedWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-nested-with-optional-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:NestedObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnNestedWithRequiredField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnNestedWithRequiredField", + "camelCase": { + "unsafeName": "getAndReturnNestedWithRequiredField", + "safeName": "getAndReturnNestedWithRequiredField" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_required_field", + "safeName": "get_and_return_nested_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD", + "safeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithRequiredField", + "safeName": "GetAndReturnNestedWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-nested-with-required-field/{string}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:NestedObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnNestedWithRequiredFieldAsList": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnNestedWithRequiredFieldAsList", + "camelCase": { + "unsafeName": "getAndReturnNestedWithRequiredFieldAsList", + "safeName": "getAndReturnNestedWithRequiredFieldAsList" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_required_field_as_list", + "safeName": "get_and_return_nested_with_required_field_as_list" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD_AS_LIST", + "safeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD_AS_LIST" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithRequiredFieldAsList", + "safeName": "GetAndReturnNestedWithRequiredFieldAsList" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-nested-with-required-field-list" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "list", + "value": { + "type": "named", + "value": "type_types/object:NestedObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithUnknownField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithUnknownField", + "camelCase": { + "unsafeName": "getAndReturnWithUnknownField", + "safeName": "getAndReturnWithUnknownField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_unknown_field", + "safeName": "get_and_return_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_UNKNOWN_FIELD", + "safeName": "GET_AND_RETURN_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithUnknownField", + "safeName": "GetAndReturnWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-unknown-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithUnknownField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithDatetimeLikeString": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithDatetimeLikeString", + "camelCase": { + "unsafeName": "getAndReturnWithDatetimeLikeString", + "safeName": "getAndReturnWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_datetime_like_string", + "safeName": "get_and_return_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_DATETIME_LIKE_STRING", + "safeName": "GET_AND_RETURN_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithDatetimeLikeString", + "safeName": "GetAndReturnWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-datetime-like-string" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithDatetimeLikeString" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/pagination.listItems": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "listItems", + "camelCase": { + "unsafeName": "listItems", + "safeName": "listItems" + }, + "snakeCase": { + "unsafeName": "list_items", + "safeName": "list_items" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_ITEMS", + "safeName": "LIST_ITEMS" + }, + "pascalCase": { + "unsafeName": "ListItems", + "safeName": "ListItems" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + } + }, + "location": { + "method": "GET", + "path": "/pagination" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListItemsRequest", + "camelCase": { + "unsafeName": "listItemsRequest", + "safeName": "listItemsRequest" + }, + "snakeCase": { + "unsafeName": "list_items_request", + "safeName": "list_items_request" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_ITEMS_REQUEST", + "safeName": "LIST_ITEMS_REQUEST" + }, + "pascalCase": { + "unsafeName": "ListItemsRequest", + "safeName": "ListItemsRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "cursor", + "camelCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "snakeCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "screamingSnakeCase": { + "unsafeName": "CURSOR", + "safeName": "CURSOR" + }, + "pascalCase": { + "unsafeName": "Cursor", + "safeName": "Cursor" + } + }, + "wireValue": "cursor" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithPath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithPath", + "camelCase": { + "unsafeName": "getWithPath", + "safeName": "getWithPath" + }, + "snakeCase": { + "unsafeName": "get_with_path", + "safeName": "get_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH", + "safeName": "GET_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithPath", + "safeName": "GetWithPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path/{param}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithInlinePath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithInlinePath", + "camelCase": { + "unsafeName": "getWithInlinePath", + "safeName": "getWithInlinePath" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path", + "safeName": "get_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH", + "safeName": "GET_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePath", + "safeName": "GetWithInlinePath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "getWithInlinePath", + "camelCase": { + "unsafeName": "getWithInlinePath", + "safeName": "getWithInlinePath" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path", + "safeName": "get_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH", + "safeName": "GET_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePath", + "safeName": "GetWithInlinePath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": true + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithQuery", + "camelCase": { + "unsafeName": "getWithQuery", + "safeName": "getWithQuery" + }, + "snakeCase": { + "unsafeName": "get_with_query", + "safeName": "get_with_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_QUERY", + "safeName": "GET_WITH_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithQuery", + "safeName": "GetWithQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetWithQuery", + "camelCase": { + "unsafeName": "getWithQuery", + "safeName": "getWithQuery" + }, + "snakeCase": { + "unsafeName": "get_with_query", + "safeName": "get_with_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_QUERY", + "safeName": "GET_WITH_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithQuery", + "safeName": "GetWithQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithAllowMultipleQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithAllowMultipleQuery", + "camelCase": { + "unsafeName": "getWithAllowMultipleQuery", + "safeName": "getWithAllowMultipleQuery" + }, + "snakeCase": { + "unsafeName": "get_with_allow_multiple_query", + "safeName": "get_with_allow_multiple_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_ALLOW_MULTIPLE_QUERY", + "safeName": "GET_WITH_ALLOW_MULTIPLE_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithAllowMultipleQuery", + "safeName": "GetWithAllowMultipleQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetWithMultipleQuery", + "camelCase": { + "unsafeName": "getWithMultipleQuery", + "safeName": "getWithMultipleQuery" + }, + "snakeCase": { + "unsafeName": "get_with_multiple_query", + "safeName": "get_with_multiple_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_MULTIPLE_QUERY", + "safeName": "GET_WITH_MULTIPLE_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithMultipleQuery", + "safeName": "GetWithMultipleQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "typeReference": { + "type": "list", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithPathAndQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithPathAndQuery", + "camelCase": { + "unsafeName": "getWithPathAndQuery", + "safeName": "getWithPathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_path_and_query", + "safeName": "get_with_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH_AND_QUERY", + "safeName": "GET_WITH_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithPathAndQuery", + "safeName": "GetWithPathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path-query/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetWithPathAndQuery", + "camelCase": { + "unsafeName": "getWithPathAndQuery", + "safeName": "getWithPathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_path_and_query", + "safeName": "get_with_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH_AND_QUERY", + "safeName": "GET_WITH_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithPathAndQuery", + "safeName": "GetWithPathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithInlinePathAndQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithInlinePathAndQuery", + "camelCase": { + "unsafeName": "getWithInlinePathAndQuery", + "safeName": "getWithInlinePathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path_and_query", + "safeName": "get_with_inline_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH_AND_QUERY", + "safeName": "GET_WITH_INLINE_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePathAndQuery", + "safeName": "GetWithInlinePathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path-query/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "getWithInlinePathAndQuery", + "camelCase": { + "unsafeName": "getWithInlinePathAndQuery", + "safeName": "getWithInlinePathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path_and_query", + "safeName": "get_with_inline_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH_AND_QUERY", + "safeName": "GET_WITH_INLINE_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePathAndQuery", + "safeName": "GetWithInlinePathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.modifyWithPath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "modifyWithPath", + "camelCase": { + "unsafeName": "modifyWithPath", + "safeName": "modifyWithPath" + }, + "snakeCase": { + "unsafeName": "modify_with_path", + "safeName": "modify_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_WITH_PATH", + "safeName": "MODIFY_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyWithPath", + "safeName": "ModifyWithPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/params/path/{param}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.modifyWithInlinePath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "modifyWithInlinePath", + "camelCase": { + "unsafeName": "modifyWithInlinePath", + "safeName": "modifyWithInlinePath" + }, + "snakeCase": { + "unsafeName": "modify_with_inline_path", + "safeName": "modify_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_WITH_INLINE_PATH", + "safeName": "MODIFY_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyWithInlinePath", + "safeName": "ModifyWithInlinePath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/params/path/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ModifyResourceAtInlinedPath", + "camelCase": { + "unsafeName": "modifyResourceAtInlinedPath", + "safeName": "modifyResourceAtInlinedPath" + }, + "snakeCase": { + "unsafeName": "modify_resource_at_inlined_path", + "safeName": "modify_resource_at_inlined_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_RESOURCE_AT_INLINED_PATH", + "safeName": "MODIFY_RESOURCE_AT_INLINED_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyResourceAtInlinedPath", + "safeName": "ModifyResourceAtInlinedPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": { + "type": "referenced", + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "bodyType": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.uploadWithPath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "uploadWithPath", + "camelCase": { + "unsafeName": "uploadWithPath", + "safeName": "uploadWithPath" + }, + "snakeCase": { + "unsafeName": "upload_with_path", + "safeName": "upload_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "UPLOAD_WITH_PATH", + "safeName": "UPLOAD_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "UploadWithPath", + "safeName": "UploadWithPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "POST", + "path": "/params/path/{param}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "bytes" + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnString": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnString", + "camelCase": { + "unsafeName": "getAndReturnString", + "safeName": "getAndReturnString" + }, + "snakeCase": { + "unsafeName": "get_and_return_string", + "safeName": "get_and_return_string" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_STRING", + "safeName": "GET_AND_RETURN_STRING" + }, + "pascalCase": { + "unsafeName": "GetAndReturnString", + "safeName": "GetAndReturnString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/string" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnInt": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnInt", + "camelCase": { + "unsafeName": "getAndReturnInt", + "safeName": "getAndReturnInt" + }, + "snakeCase": { + "unsafeName": "get_and_return_int", + "safeName": "get_and_return_int" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_INT", + "safeName": "GET_AND_RETURN_INT" + }, + "pascalCase": { + "unsafeName": "GetAndReturnInt", + "safeName": "GetAndReturnInt" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/integer" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "INTEGER" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnLong": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnLong", + "camelCase": { + "unsafeName": "getAndReturnLong", + "safeName": "getAndReturnLong" + }, + "snakeCase": { + "unsafeName": "get_and_return_long", + "safeName": "get_and_return_long" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LONG", + "safeName": "GET_AND_RETURN_LONG" + }, + "pascalCase": { + "unsafeName": "GetAndReturnLong", + "safeName": "GetAndReturnLong" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/long" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "LONG" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnDouble": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnDouble", + "camelCase": { + "unsafeName": "getAndReturnDouble", + "safeName": "getAndReturnDouble" + }, + "snakeCase": { + "unsafeName": "get_and_return_double", + "safeName": "get_and_return_double" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DOUBLE", + "safeName": "GET_AND_RETURN_DOUBLE" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDouble", + "safeName": "GetAndReturnDouble" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/double" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "DOUBLE" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnBool": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnBool", + "camelCase": { + "unsafeName": "getAndReturnBool", + "safeName": "getAndReturnBool" + }, + "snakeCase": { + "unsafeName": "get_and_return_bool", + "safeName": "get_and_return_bool" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_BOOL", + "safeName": "GET_AND_RETURN_BOOL" + }, + "pascalCase": { + "unsafeName": "GetAndReturnBool", + "safeName": "GetAndReturnBool" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/boolean" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnDatetime": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnDatetime", + "camelCase": { + "unsafeName": "getAndReturnDatetime", + "safeName": "getAndReturnDatetime" + }, + "snakeCase": { + "unsafeName": "get_and_return_datetime", + "safeName": "get_and_return_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DATETIME", + "safeName": "GET_AND_RETURN_DATETIME" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDatetime", + "safeName": "GetAndReturnDatetime" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/datetime" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "DATE_TIME" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnDate": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnDate", + "camelCase": { + "unsafeName": "getAndReturnDate", + "safeName": "getAndReturnDate" + }, + "snakeCase": { + "unsafeName": "get_and_return_date", + "safeName": "get_and_return_date" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DATE", + "safeName": "GET_AND_RETURN_DATE" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDate", + "safeName": "GetAndReturnDate" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/date" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "DATE" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnUUID": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnUUID", + "camelCase": { + "unsafeName": "getAndReturnUUID", + "safeName": "getAndReturnUUID" + }, + "snakeCase": { + "unsafeName": "get_and_return_uuid", + "safeName": "get_and_return_uuid" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_UUID", + "safeName": "GET_AND_RETURN_UUID" + }, + "pascalCase": { + "unsafeName": "GetAndReturnUUID", + "safeName": "GetAndReturnUUID" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/uuid" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "UUID" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnBase64": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnBase64", + "camelCase": { + "unsafeName": "getAndReturnBase64", + "safeName": "getAndReturnBase64" + }, + "snakeCase": { + "unsafeName": "get_and_return_base64", + "safeName": "get_and_return_base64" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_BASE64", + "safeName": "GET_AND_RETURN_BASE64" + }, + "pascalCase": { + "unsafeName": "GetAndReturnBase64", + "safeName": "GetAndReturnBase64" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/base64" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "BASE_64" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/put.add": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "add", + "camelCase": { + "unsafeName": "add", + "safeName": "add" + }, + "snakeCase": { + "unsafeName": "add", + "safeName": "add" + }, + "screamingSnakeCase": { + "unsafeName": "ADD", + "safeName": "ADD" + }, + "pascalCase": { + "unsafeName": "Add", + "safeName": "Add" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "PutRequest", + "camelCase": { + "unsafeName": "putRequest", + "safeName": "putRequest" + }, + "snakeCase": { + "unsafeName": "put_request", + "safeName": "put_request" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_REQUEST", + "safeName": "PUT_REQUEST" + }, + "pascalCase": { + "unsafeName": "PutRequest", + "safeName": "PutRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": true + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/union.getAndReturnUnion": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnUnion", + "camelCase": { + "unsafeName": "getAndReturnUnion", + "safeName": "getAndReturnUnion" + }, + "snakeCase": { + "unsafeName": "get_and_return_union", + "safeName": "get_and_return_union" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_UNION", + "safeName": "GET_AND_RETURN_UNION" + }, + "pascalCase": { + "unsafeName": "GetAndReturnUnion", + "safeName": "GetAndReturnUnion" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "location": { + "method": "POST", + "path": "/union" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/union:Animal" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.withMixedCase": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "withMixedCase", + "camelCase": { + "unsafeName": "withMixedCase", + "safeName": "withMixedCase" + }, + "snakeCase": { + "unsafeName": "with_mixed_case", + "safeName": "with_mixed_case" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_MIXED_CASE", + "safeName": "WITH_MIXED_CASE" + }, + "pascalCase": { + "unsafeName": "WithMixedCase", + "safeName": "WithMixedCase" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/MixedCase" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.noEndingSlash": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "noEndingSlash", + "camelCase": { + "unsafeName": "noEndingSlash", + "safeName": "noEndingSlash" + }, + "snakeCase": { + "unsafeName": "no_ending_slash", + "safeName": "no_ending_slash" + }, + "screamingSnakeCase": { + "unsafeName": "NO_ENDING_SLASH", + "safeName": "NO_ENDING_SLASH" + }, + "pascalCase": { + "unsafeName": "NoEndingSlash", + "safeName": "NoEndingSlash" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/no-ending-slash" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.withEndingSlash": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "withEndingSlash", + "camelCase": { + "unsafeName": "withEndingSlash", + "safeName": "withEndingSlash" + }, + "snakeCase": { + "unsafeName": "with_ending_slash", + "safeName": "with_ending_slash" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_ENDING_SLASH", + "safeName": "WITH_ENDING_SLASH" + }, + "pascalCase": { + "unsafeName": "WithEndingSlash", + "safeName": "WithEndingSlash" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/with-ending-slash/" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.withUnderscores": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "withUnderscores", + "camelCase": { + "unsafeName": "withUnderscores", + "safeName": "withUnderscores" + }, + "snakeCase": { + "unsafeName": "with_underscores", + "safeName": "with_underscores" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_UNDERSCORES", + "safeName": "WITH_UNDERSCORES" + }, + "pascalCase": { + "unsafeName": "WithUnderscores", + "safeName": "WithUnderscores" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/with_underscores" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_inlined-requests.postWithObjectBodyandResponse": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postWithObjectBodyandResponse", + "camelCase": { + "unsafeName": "postWithObjectBodyandResponse", + "safeName": "postWithObjectBodyandResponse" + }, + "snakeCase": { + "unsafeName": "post_with_object_bodyand_response", + "safeName": "post_with_object_bodyand_response" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_OBJECT_BODYAND_RESPONSE", + "safeName": "POST_WITH_OBJECT_BODYAND_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PostWithObjectBodyandResponse", + "safeName": "PostWithObjectBodyandResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + ], + "packagePath": [], + "file": { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + } + }, + "location": { + "method": "POST", + "path": "/req-bodies/object" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "PostWithObjectBody", + "camelCase": { + "unsafeName": "postWithObjectBody", + "safeName": "postWithObjectBody" + }, + "snakeCase": { + "unsafeName": "post_with_object_body", + "safeName": "post_with_object_body" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_OBJECT_BODY", + "safeName": "POST_WITH_OBJECT_BODY" + }, + "pascalCase": { + "unsafeName": "PostWithObjectBody", + "safeName": "PostWithObjectBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + ], + "packagePath": [], + "file": { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "typeReference": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_no-auth.postWithNoAuth": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postWithNoAuth", + "camelCase": { + "unsafeName": "postWithNoAuth", + "safeName": "postWithNoAuth" + }, + "snakeCase": { + "unsafeName": "post_with_no_auth", + "safeName": "post_with_no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_NO_AUTH", + "safeName": "POST_WITH_NO_AUTH" + }, + "pascalCase": { + "unsafeName": "PostWithNoAuth", + "safeName": "PostWithNoAuth" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + } + }, + "location": { + "method": "POST", + "path": "/no-auth" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "unknown" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_no-req-body.getWithNoRequestBody": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithNoRequestBody", + "camelCase": { + "unsafeName": "getWithNoRequestBody", + "safeName": "getWithNoRequestBody" + }, + "snakeCase": { + "unsafeName": "get_with_no_request_body", + "safeName": "get_with_no_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_NO_REQUEST_BODY", + "safeName": "GET_WITH_NO_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "GetWithNoRequestBody", + "safeName": "GetWithNoRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + } + }, + "location": { + "method": "GET", + "path": "/no-req-body" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_no-req-body.postWithNoRequestBody": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postWithNoRequestBody", + "camelCase": { + "unsafeName": "postWithNoRequestBody", + "safeName": "postWithNoRequestBody" + }, + "snakeCase": { + "unsafeName": "post_with_no_request_body", + "safeName": "post_with_no_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_NO_REQUEST_BODY", + "safeName": "POST_WITH_NO_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "PostWithNoRequestBody", + "safeName": "PostWithNoRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + } + }, + "location": { + "method": "POST", + "path": "/no-req-body" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_req-with-headers.getWithCustomHeader": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithCustomHeader", + "camelCase": { + "unsafeName": "getWithCustomHeader", + "safeName": "getWithCustomHeader" + }, + "snakeCase": { + "unsafeName": "get_with_custom_header", + "safeName": "get_with_custom_header" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_CUSTOM_HEADER", + "safeName": "GET_WITH_CUSTOM_HEADER" + }, + "pascalCase": { + "unsafeName": "GetWithCustomHeader", + "safeName": "GetWithCustomHeader" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + ], + "packagePath": [], + "file": { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + } + }, + "location": { + "method": "POST", + "path": "/test-headers/custom-header" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ReqWithHeaders", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + ], + "packagePath": [], + "file": { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [ + { + "name": { + "name": { + "originalName": "X-TEST-SERVICE-HEADER", + "camelCase": { + "unsafeName": "xTestServiceHeader", + "safeName": "xTestServiceHeader" + }, + "snakeCase": { + "unsafeName": "x_test_service_header", + "safeName": "x_test_service_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_SERVICE_HEADER", + "safeName": "X_TEST_SERVICE_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestServiceHeader", + "safeName": "XTestServiceHeader" + } + }, + "wireValue": "X-TEST-SERVICE-HEADER" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "X-TEST-ENDPOINT-HEADER", + "camelCase": { + "unsafeName": "xTestEndpointHeader", + "safeName": "xTestEndpointHeader" + }, + "snakeCase": { + "unsafeName": "x_test_endpoint_header", + "safeName": "x_test_endpoint_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_ENDPOINT_HEADER", + "safeName": "X_TEST_ENDPOINT_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestEndpointHeader", + "safeName": "XTestEndpointHeader" + } + }, + "wireValue": "X-TEST-ENDPOINT-HEADER" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "referenced", + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "bodyType": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "generatorConfig": null +} \ No newline at end of file diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json new file mode 100644 index 000000000000..452e7c137a57 --- /dev/null +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/go-deterministic-ordering.json @@ -0,0 +1,114182 @@ +{ + "selfHosted": false, + "fdrApiDefinitionId": null, + "apiVersion": null, + "apiName": { + "originalName": "exhaustive", + "camelCase": { + "unsafeName": "exhaustive", + "safeName": "exhaustive" + }, + "snakeCase": { + "unsafeName": "exhaustive", + "safeName": "exhaustive" + }, + "screamingSnakeCase": { + "unsafeName": "EXHAUSTIVE", + "safeName": "EXHAUSTIVE" + }, + "pascalCase": { + "unsafeName": "Exhaustive", + "safeName": "Exhaustive" + } + }, + "apiDisplayName": null, + "apiDocs": null, + "auth": { + "requirement": "ALL", + "schemes": [ + { + "_type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + }, + "tokenEnvVar": null, + "key": "bearer", + "docs": null + } + ], + "docs": null + }, + "headers": [], + "idempotencyHeaders": [], + "types": { + "type_endpoints/pagination:PaginatedResponse": { + "inline": null, + "name": { + "name": { + "originalName": "PaginatedResponse", + "camelCase": { + "unsafeName": "paginatedResponse", + "safeName": "paginatedResponse" + }, + "snakeCase": { + "unsafeName": "paginated_response", + "safeName": "paginated_response" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATED_RESPONSE", + "safeName": "PAGINATED_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PaginatedResponse", + "safeName": "PaginatedResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/pagination:PaginatedResponse" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "items", + "camelCase": { + "unsafeName": "items", + "safeName": "items" + }, + "snakeCase": { + "unsafeName": "items", + "safeName": "items" + }, + "screamingSnakeCase": { + "unsafeName": "ITEMS", + "safeName": "ITEMS" + }, + "pascalCase": { + "unsafeName": "Items", + "safeName": "Items" + } + }, + "wireValue": "items" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "next", + "camelCase": { + "unsafeName": "next", + "safeName": "next" + }, + "snakeCase": { + "unsafeName": "next", + "safeName": "next" + }, + "screamingSnakeCase": { + "unsafeName": "NEXT", + "safeName": "NEXT" + }, + "pascalCase": { + "unsafeName": "Next", + "safeName": "Next" + } + }, + "wireValue": "next" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_types/object:ObjectWithRequiredField" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_endpoints/put:Error": { + "inline": null, + "name": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "category", + "camelCase": { + "unsafeName": "category", + "safeName": "category" + }, + "snakeCase": { + "unsafeName": "category", + "safeName": "category" + }, + "screamingSnakeCase": { + "unsafeName": "CATEGORY", + "safeName": "CATEGORY" + }, + "pascalCase": { + "unsafeName": "Category", + "safeName": "Category" + } + }, + "wireValue": "category" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ErrorCategory", + "camelCase": { + "unsafeName": "errorCategory", + "safeName": "errorCategory" + }, + "snakeCase": { + "unsafeName": "error_category", + "safeName": "error_category" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CATEGORY", + "safeName": "ERROR_CATEGORY" + }, + "pascalCase": { + "unsafeName": "ErrorCategory", + "safeName": "ErrorCategory" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCategory", + "default": null, + "inline": null + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "code", + "camelCase": { + "unsafeName": "code", + "safeName": "code" + }, + "snakeCase": { + "unsafeName": "code", + "safeName": "code" + }, + "screamingSnakeCase": { + "unsafeName": "CODE", + "safeName": "CODE" + }, + "pascalCase": { + "unsafeName": "Code", + "safeName": "Code" + } + }, + "wireValue": "code" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ErrorCode", + "camelCase": { + "unsafeName": "errorCode", + "safeName": "errorCode" + }, + "snakeCase": { + "unsafeName": "error_code", + "safeName": "error_code" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CODE", + "safeName": "ERROR_CODE" + }, + "pascalCase": { + "unsafeName": "ErrorCode", + "safeName": "ErrorCode" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCode", + "default": null, + "inline": null + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "detail", + "camelCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "snakeCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "screamingSnakeCase": { + "unsafeName": "DETAIL", + "safeName": "DETAIL" + }, + "pascalCase": { + "unsafeName": "Detail", + "safeName": "Detail" + } + }, + "wireValue": "detail" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "field", + "camelCase": { + "unsafeName": "field", + "safeName": "field" + }, + "snakeCase": { + "unsafeName": "field", + "safeName": "field" + }, + "screamingSnakeCase": { + "unsafeName": "FIELD", + "safeName": "FIELD" + }, + "pascalCase": { + "unsafeName": "Field", + "safeName": "Field" + } + }, + "wireValue": "field" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_endpoints/put:ErrorCategory", + "type_endpoints/put:ErrorCode" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_endpoints/put:ErrorCategory": { + "inline": null, + "name": { + "name": { + "originalName": "ErrorCategory", + "camelCase": { + "unsafeName": "errorCategory", + "safeName": "errorCategory" + }, + "snakeCase": { + "unsafeName": "error_category", + "safeName": "error_category" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CATEGORY", + "safeName": "ERROR_CATEGORY" + }, + "pascalCase": { + "unsafeName": "ErrorCategory", + "safeName": "ErrorCategory" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCategory" + }, + "shape": { + "_type": "enum", + "default": null, + "values": [ + { + "name": { + "name": { + "originalName": "API_ERROR", + "camelCase": { + "unsafeName": "apiError", + "safeName": "apiError" + }, + "snakeCase": { + "unsafeName": "api_error", + "safeName": "api_error" + }, + "screamingSnakeCase": { + "unsafeName": "API_ERROR", + "safeName": "API_ERROR" + }, + "pascalCase": { + "unsafeName": "APIError", + "safeName": "APIError" + } + }, + "wireValue": "API_ERROR" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "AUTHENTICATION_ERROR", + "camelCase": { + "unsafeName": "authenticationError", + "safeName": "authenticationError" + }, + "snakeCase": { + "unsafeName": "authentication_error", + "safeName": "authentication_error" + }, + "screamingSnakeCase": { + "unsafeName": "AUTHENTICATION_ERROR", + "safeName": "AUTHENTICATION_ERROR" + }, + "pascalCase": { + "unsafeName": "AuthenticationError", + "safeName": "AuthenticationError" + } + }, + "wireValue": "AUTHENTICATION_ERROR" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "INVALID_REQUEST_ERROR", + "camelCase": { + "unsafeName": "invalidRequestError", + "safeName": "invalidRequestError" + }, + "snakeCase": { + "unsafeName": "invalid_request_error", + "safeName": "invalid_request_error" + }, + "screamingSnakeCase": { + "unsafeName": "INVALID_REQUEST_ERROR", + "safeName": "INVALID_REQUEST_ERROR" + }, + "pascalCase": { + "unsafeName": "InvalidRequestError", + "safeName": "InvalidRequestError" + } + }, + "wireValue": "INVALID_REQUEST_ERROR" + }, + "availability": null, + "docs": null + } + ] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_endpoints/put:ErrorCode": { + "inline": null, + "name": { + "name": { + "originalName": "ErrorCode", + "camelCase": { + "unsafeName": "errorCode", + "safeName": "errorCode" + }, + "snakeCase": { + "unsafeName": "error_code", + "safeName": "error_code" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CODE", + "safeName": "ERROR_CODE" + }, + "pascalCase": { + "unsafeName": "ErrorCode", + "safeName": "ErrorCode" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCode" + }, + "shape": { + "_type": "enum", + "default": null, + "values": [ + { + "name": { + "name": { + "originalName": "INTERNAL_SERVER_ERROR", + "camelCase": { + "unsafeName": "internalServerError", + "safeName": "internalServerError" + }, + "snakeCase": { + "unsafeName": "internal_server_error", + "safeName": "internal_server_error" + }, + "screamingSnakeCase": { + "unsafeName": "INTERNAL_SERVER_ERROR", + "safeName": "INTERNAL_SERVER_ERROR" + }, + "pascalCase": { + "unsafeName": "InternalServerError", + "safeName": "InternalServerError" + } + }, + "wireValue": "INTERNAL_SERVER_ERROR" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "UNAUTHORIZED", + "camelCase": { + "unsafeName": "unauthorized", + "safeName": "unauthorized" + }, + "snakeCase": { + "unsafeName": "unauthorized", + "safeName": "unauthorized" + }, + "screamingSnakeCase": { + "unsafeName": "UNAUTHORIZED", + "safeName": "UNAUTHORIZED" + }, + "pascalCase": { + "unsafeName": "Unauthorized", + "safeName": "Unauthorized" + } + }, + "wireValue": "UNAUTHORIZED" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "FORBIDDEN", + "camelCase": { + "unsafeName": "forbidden", + "safeName": "forbidden" + }, + "snakeCase": { + "unsafeName": "forbidden", + "safeName": "forbidden" + }, + "screamingSnakeCase": { + "unsafeName": "FORBIDDEN", + "safeName": "FORBIDDEN" + }, + "pascalCase": { + "unsafeName": "Forbidden", + "safeName": "Forbidden" + } + }, + "wireValue": "FORBIDDEN" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "BAD_REQUEST", + "camelCase": { + "unsafeName": "badRequest", + "safeName": "badRequest" + }, + "snakeCase": { + "unsafeName": "bad_request", + "safeName": "bad_request" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST", + "safeName": "BAD_REQUEST" + }, + "pascalCase": { + "unsafeName": "BadRequest", + "safeName": "BadRequest" + } + }, + "wireValue": "BAD_REQUEST" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "CONFLICT", + "camelCase": { + "unsafeName": "conflict", + "safeName": "conflict" + }, + "snakeCase": { + "unsafeName": "conflict", + "safeName": "conflict" + }, + "screamingSnakeCase": { + "unsafeName": "CONFLICT", + "safeName": "CONFLICT" + }, + "pascalCase": { + "unsafeName": "Conflict", + "safeName": "Conflict" + } + }, + "wireValue": "CONFLICT" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "GONE", + "camelCase": { + "unsafeName": "gone", + "safeName": "gone" + }, + "snakeCase": { + "unsafeName": "gone", + "safeName": "gone" + }, + "screamingSnakeCase": { + "unsafeName": "GONE", + "safeName": "GONE" + }, + "pascalCase": { + "unsafeName": "Gone", + "safeName": "Gone" + } + }, + "wireValue": "GONE" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "UNPROCESSABLE_ENTITY", + "camelCase": { + "unsafeName": "unprocessableEntity", + "safeName": "unprocessableEntity" + }, + "snakeCase": { + "unsafeName": "unprocessable_entity", + "safeName": "unprocessable_entity" + }, + "screamingSnakeCase": { + "unsafeName": "UNPROCESSABLE_ENTITY", + "safeName": "UNPROCESSABLE_ENTITY" + }, + "pascalCase": { + "unsafeName": "UnprocessableEntity", + "safeName": "UnprocessableEntity" + } + }, + "wireValue": "UNPROCESSABLE_ENTITY" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "NOT_IMPLEMENTED", + "camelCase": { + "unsafeName": "notImplemented", + "safeName": "notImplemented" + }, + "snakeCase": { + "unsafeName": "not_implemented", + "safeName": "not_implemented" + }, + "screamingSnakeCase": { + "unsafeName": "NOT_IMPLEMENTED", + "safeName": "NOT_IMPLEMENTED" + }, + "pascalCase": { + "unsafeName": "NotImplemented", + "safeName": "NotImplemented" + } + }, + "wireValue": "NOT_IMPLEMENTED" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "BAD_GATEWAY", + "camelCase": { + "unsafeName": "badGateway", + "safeName": "badGateway" + }, + "snakeCase": { + "unsafeName": "bad_gateway", + "safeName": "bad_gateway" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_GATEWAY", + "safeName": "BAD_GATEWAY" + }, + "pascalCase": { + "unsafeName": "BadGateway", + "safeName": "BadGateway" + } + }, + "wireValue": "BAD_GATEWAY" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "SERVICE_UNAVAILABLE", + "camelCase": { + "unsafeName": "serviceUnavailable", + "safeName": "serviceUnavailable" + }, + "snakeCase": { + "unsafeName": "service_unavailable", + "safeName": "service_unavailable" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE_UNAVAILABLE", + "safeName": "SERVICE_UNAVAILABLE" + }, + "pascalCase": { + "unsafeName": "ServiceUnavailable", + "safeName": "ServiceUnavailable" + } + }, + "wireValue": "SERVICE_UNAVAILABLE" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "Unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "Unknown" + }, + "availability": null, + "docs": null + } + ] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_endpoints/put:PutResponse": { + "inline": null, + "name": { + "name": { + "originalName": "PutResponse", + "camelCase": { + "unsafeName": "putResponse", + "safeName": "putResponse" + }, + "snakeCase": { + "unsafeName": "put_response", + "safeName": "put_response" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_RESPONSE", + "safeName": "PUT_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PutResponse", + "safeName": "PutResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:PutResponse" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "errors", + "camelCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "snakeCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "screamingSnakeCase": { + "unsafeName": "ERRORS", + "safeName": "ERRORS" + }, + "pascalCase": { + "unsafeName": "Errors", + "safeName": "Errors" + } + }, + "wireValue": "errors" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error", + "default": null, + "inline": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_endpoints/put:Error", + "type_endpoints/put:ErrorCategory", + "type_endpoints/put:ErrorCode" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_general-errors:BadObjectRequestInfo": { + "inline": null, + "name": { + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "displayName": null, + "typeId": "type_general-errors:BadObjectRequestInfo" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "message", + "camelCase": { + "unsafeName": "message", + "safeName": "message" + }, + "snakeCase": { + "unsafeName": "message", + "safeName": "message" + }, + "screamingSnakeCase": { + "unsafeName": "MESSAGE", + "safeName": "MESSAGE" + }, + "pascalCase": { + "unsafeName": "Message", + "safeName": "Message" + } + }, + "wireValue": "message" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/docs:ObjectWithDocs": { + "inline": null, + "name": { + "name": { + "originalName": "ObjectWithDocs", + "camelCase": { + "unsafeName": "objectWithDocs", + "safeName": "objectWithDocs" + }, + "snakeCase": { + "unsafeName": "object_with_docs", + "safeName": "object_with_docs" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DOCS", + "safeName": "OBJECT_WITH_DOCS" + }, + "pascalCase": { + "unsafeName": "ObjectWithDocs", + "safeName": "ObjectWithDocs" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + }, + "displayName": null, + "typeId": "type_types/docs:ObjectWithDocs" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": "Characters that could lead to broken generated SDKs:\n\nMarkdown Escapes:\n- \\_: Escaped underscore (e.g., FOO\\_BAR)\n- \\*: Escaped asterisk\n\nJSDoc (JavaScript/TypeScript):\n- @: Used for JSDoc tags\n- {: }: Used for type definitions\n- <: >: HTML tags\n- *: Can interfere with comment blocks\n- /**: JSDoc comment start\n- ** /: JSDoc comment end\n- &: HTML entities\n\nXMLDoc (C#):\n- <: >: XML tags\n- &: ': \": <: >: XML special characters\n- {: }: Used for interpolated strings\n- ///: Comment marker\n- /**: Block comment start\n- ** /: Block comment end\n\nXMLDoc (C#) (Example of actual XML tags):\nSee the docs for more info.\nUse getValue() to retrieve the value.\nNote: when count < 10 or count > 100, special handling applies.\n\nJavadoc (Java):\n- @: Used for Javadoc tags\n- <: >: HTML tags\n- &: HTML entities\n- *: Can interfere with comment blocks\n- /**: Javadoc comment start\n- ** /: Javadoc comment end\n\nDoxygen (C++):\n- \\: Used for Doxygen commands\n- @: Alternative command prefix\n- <: >: XML/HTML tags\n- &: HTML entities\n- /**: C-style comment start\n- ** /: C-style comment end\n\nRDoc (Ruby):\n- :: Used in symbol notation\n- =: Section markers\n- #: Comment marker\n- =begin: Block comment start\n- =end: Block comment end\n- @: Instance variable prefix\n- $: Global variable prefix\n- %: String literal delimiter\n- #{: String interpolation start\n- }: String interpolation end\n\nPHPDoc (PHP):\n- @: Used for PHPDoc tags\n- {: }: Used for type definitions\n- $: Variable prefix\n- /**: PHPDoc comment start\n- ** /: PHPDoc comment end\n- *: Can interfere with comment blocks\n- &: HTML entities" + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/enum:WeatherReport": { + "inline": null, + "name": { + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "displayName": null, + "typeId": "type_types/enum:WeatherReport" + }, + "shape": { + "_type": "enum", + "default": null, + "values": [ + { + "name": { + "name": { + "originalName": "SUNNY", + "camelCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "snakeCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "screamingSnakeCase": { + "unsafeName": "SUNNY", + "safeName": "SUNNY" + }, + "pascalCase": { + "unsafeName": "Sunny", + "safeName": "Sunny" + } + }, + "wireValue": "SUNNY" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "CLOUDY", + "camelCase": { + "unsafeName": "cloudy", + "safeName": "cloudy" + }, + "snakeCase": { + "unsafeName": "cloudy", + "safeName": "cloudy" + }, + "screamingSnakeCase": { + "unsafeName": "CLOUDY", + "safeName": "CLOUDY" + }, + "pascalCase": { + "unsafeName": "Cloudy", + "safeName": "Cloudy" + } + }, + "wireValue": "CLOUDY" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "RAINING", + "camelCase": { + "unsafeName": "raining", + "safeName": "raining" + }, + "snakeCase": { + "unsafeName": "raining", + "safeName": "raining" + }, + "screamingSnakeCase": { + "unsafeName": "RAINING", + "safeName": "RAINING" + }, + "pascalCase": { + "unsafeName": "Raining", + "safeName": "Raining" + } + }, + "wireValue": "RAINING" + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "SNOWING", + "camelCase": { + "unsafeName": "snowing", + "safeName": "snowing" + }, + "snakeCase": { + "unsafeName": "snowing", + "safeName": "snowing" + }, + "screamingSnakeCase": { + "unsafeName": "SNOWING", + "safeName": "SNOWING" + }, + "pascalCase": { + "unsafeName": "Snowing", + "safeName": "Snowing" + } + }, + "wireValue": "SNOWING" + }, + "availability": null, + "docs": null + } + ] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:ObjectWithOptionalField": { + "inline": null, + "name": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": "This is a rather long descriptor of this single field in a more complex type. If you ask me I think this is a pretty good description for this field all things considered." + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:ObjectWithRequiredField": { + "inline": null, + "name": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:ObjectWithMapOfMap": { + "inline": null, + "name": { + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:NestedObjectWithOptionalField": { + "inline": null, + "name": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_types/object:ObjectWithOptionalField" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:NestedObjectWithRequiredField": { + "inline": null, + "name": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_types/object:ObjectWithOptionalField" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:DoubleOptional": { + "inline": null, + "name": { + "name": { + "originalName": "DoubleOptional", + "camelCase": { + "unsafeName": "doubleOptional", + "safeName": "doubleOptional" + }, + "snakeCase": { + "unsafeName": "double_optional", + "safeName": "double_optional" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE_OPTIONAL", + "safeName": "DOUBLE_OPTIONAL" + }, + "pascalCase": { + "unsafeName": "DoubleOptional", + "safeName": "DoubleOptional" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:DoubleOptional" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "optionalAlias", + "camelCase": { + "unsafeName": "optionalAlias", + "safeName": "optionalAlias" + }, + "snakeCase": { + "unsafeName": "optional_alias", + "safeName": "optional_alias" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_ALIAS", + "safeName": "OPTIONAL_ALIAS" + }, + "pascalCase": { + "unsafeName": "OptionalAlias", + "safeName": "OptionalAlias" + } + }, + "wireValue": "optionalAlias" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "named", + "name": { + "originalName": "OptionalAlias", + "camelCase": { + "unsafeName": "optionalAlias", + "safeName": "optionalAlias" + }, + "snakeCase": { + "unsafeName": "optional_alias", + "safeName": "optional_alias" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_ALIAS", + "safeName": "OPTIONAL_ALIAS" + }, + "pascalCase": { + "unsafeName": "OptionalAlias", + "safeName": "OptionalAlias" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:OptionalAlias", + "default": null, + "inline": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [ + "type_types/object:OptionalAlias" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:OptionalAlias": { + "inline": null, + "name": { + "name": { + "originalName": "OptionalAlias", + "camelCase": { + "unsafeName": "optionalAlias", + "safeName": "optionalAlias" + }, + "snakeCase": { + "unsafeName": "optional_alias", + "safeName": "optional_alias" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_ALIAS", + "safeName": "OPTIONAL_ALIAS" + }, + "pascalCase": { + "unsafeName": "OptionalAlias", + "safeName": "OptionalAlias" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:OptionalAlias" + }, + "shape": { + "_type": "alias", + "aliasOf": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "resolvedType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/object:ObjectWithDatetimeLikeString": { + "inline": null, + "name": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "datetimeLikeString", + "camelCase": { + "unsafeName": "datetimeLikeString", + "safeName": "datetimeLikeString" + }, + "snakeCase": { + "unsafeName": "datetime_like_string", + "safeName": "datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING", + "safeName": "DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeString", + "safeName": "DatetimeLikeString" + } + }, + "wireValue": "datetimeLikeString" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": "A string field that happens to contain a datetime-like value" + }, + { + "name": { + "name": { + "originalName": "actualDatetime", + "camelCase": { + "unsafeName": "actualDatetime", + "safeName": "actualDatetime" + }, + "snakeCase": { + "unsafeName": "actual_datetime", + "safeName": "actual_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "ACTUAL_DATETIME", + "safeName": "ACTUAL_DATETIME" + }, + "pascalCase": { + "unsafeName": "ActualDatetime", + "safeName": "ActualDatetime" + } + }, + "wireValue": "actualDatetime" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": "An actual datetime field for comparison" + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": "This type tests that string fields containing datetime-like values\nare NOT reformatted by the wire test generator. The string field\nshould preserve its exact value even if it looks like a datetime." + }, + "type_types/object:ObjectWithUnknownField": { + "inline": null, + "name": { + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "unknown" + }, + "valueType": { + "_type": "unknown" + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": "Tests that unknown/any values containing backslashes in map keys\nare properly escaped in Go string literals." + }, + "type_types/union:Animal": { + "inline": null, + "name": { + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Animal" + }, + "shape": { + "_type": "union", + "discriminant": { + "name": { + "originalName": "animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "wireValue": "animal" + }, + "extends": [], + "baseProperties": [], + "types": [ + { + "discriminantValue": { + "name": { + "originalName": "dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "wireValue": "dog" + }, + "shape": { + "_type": "samePropertiesAsObject", + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Dog" + }, + "displayName": null, + "availability": null, + "docs": null + }, + { + "discriminantValue": { + "name": { + "originalName": "cat", + "camelCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "snakeCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "screamingSnakeCase": { + "unsafeName": "CAT", + "safeName": "CAT" + }, + "pascalCase": { + "unsafeName": "Cat", + "safeName": "Cat" + } + }, + "wireValue": "cat" + }, + "shape": { + "_type": "samePropertiesAsObject", + "name": { + "originalName": "Cat", + "camelCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "snakeCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "screamingSnakeCase": { + "unsafeName": "CAT", + "safeName": "CAT" + }, + "pascalCase": { + "unsafeName": "Cat", + "safeName": "Cat" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Cat" + }, + "displayName": null, + "availability": null, + "docs": null + } + ], + "discriminatorContext": "data" + }, + "referencedTypes": [ + "type_types/union:Dog", + "type_types/union:Cat" + ], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/union:Dog": { + "inline": null, + "name": { + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Dog" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "likesToWoof", + "camelCase": { + "unsafeName": "likesToWoof", + "safeName": "likesToWoof" + }, + "snakeCase": { + "unsafeName": "likes_to_woof", + "safeName": "likes_to_woof" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_WOOF", + "safeName": "LIKES_TO_WOOF" + }, + "pascalCase": { + "unsafeName": "LikesToWoof", + "safeName": "LikesToWoof" + } + }, + "wireValue": "likesToWoof" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/union:Cat": { + "inline": null, + "name": { + "name": { + "originalName": "Cat", + "camelCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "snakeCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "screamingSnakeCase": { + "unsafeName": "CAT", + "safeName": "CAT" + }, + "pascalCase": { + "unsafeName": "Cat", + "safeName": "Cat" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Cat" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "likesToMeow", + "camelCase": { + "unsafeName": "likesToMeow", + "safeName": "likesToMeow" + }, + "snakeCase": { + "unsafeName": "likes_to_meow", + "safeName": "likes_to_meow" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_MEOW", + "safeName": "LIKES_TO_MEOW" + }, + "pascalCase": { + "unsafeName": "LikesToMeow", + "safeName": "LikesToMeow" + } + }, + "wireValue": "likesToMeow" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_types/union:MixedType": { + "inline": null, + "name": { + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType" + }, + "shape": { + "_type": "undiscriminatedUnion", + "members": [ + { + "type": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + }, + "docs": null + }, + { + "type": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "docs": null + }, + { + "type": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null + }, + { + "type": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null + } + ] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + } + }, + "errors": { + "error_general-errors:BadRequestBody": { + "name": { + "name": { + "originalName": "BadRequestBody", + "camelCase": { + "unsafeName": "badRequestBody", + "safeName": "badRequestBody" + }, + "snakeCase": { + "unsafeName": "bad_request_body", + "safeName": "bad_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST_BODY", + "safeName": "BAD_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "BadRequestBody", + "safeName": "BadRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "errorId": "error_general-errors:BadRequestBody" + }, + "discriminantValue": { + "name": { + "originalName": "BadRequestBody", + "camelCase": { + "unsafeName": "badRequestBody", + "safeName": "badRequestBody" + }, + "snakeCase": { + "unsafeName": "bad_request_body", + "safeName": "bad_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST_BODY", + "safeName": "BAD_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "BadRequestBody", + "safeName": "BadRequestBody" + } + }, + "wireValue": "BadRequestBody" + }, + "statusCode": 400, + "isWildcardStatusCode": null, + "type": { + "_type": "named", + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "displayName": null, + "typeId": "type_general-errors:BadObjectRequestInfo", + "default": null, + "inline": null + }, + "examples": [], + "v2Examples": null, + "displayName": null, + "headers": [], + "docs": null + }, + "error_types/enum:ErrorWithEnumBody": { + "name": { + "name": { + "originalName": "ErrorWithEnumBody", + "camelCase": { + "unsafeName": "errorWithEnumBody", + "safeName": "errorWithEnumBody" + }, + "snakeCase": { + "unsafeName": "error_with_enum_body", + "safeName": "error_with_enum_body" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_WITH_ENUM_BODY", + "safeName": "ERROR_WITH_ENUM_BODY" + }, + "pascalCase": { + "unsafeName": "ErrorWithEnumBody", + "safeName": "ErrorWithEnumBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "errorId": "error_types/enum:ErrorWithEnumBody" + }, + "discriminantValue": { + "name": { + "originalName": "ErrorWithEnumBody", + "camelCase": { + "unsafeName": "errorWithEnumBody", + "safeName": "errorWithEnumBody" + }, + "snakeCase": { + "unsafeName": "error_with_enum_body", + "safeName": "error_with_enum_body" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_WITH_ENUM_BODY", + "safeName": "ERROR_WITH_ENUM_BODY" + }, + "pascalCase": { + "unsafeName": "ErrorWithEnumBody", + "safeName": "ErrorWithEnumBody" + } + }, + "wireValue": "ErrorWithEnumBody" + }, + "statusCode": 400, + "isWildcardStatusCode": null, + "type": { + "_type": "named", + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "displayName": null, + "typeId": "type_types/enum:WeatherReport", + "default": null, + "inline": null + }, + "examples": [], + "v2Examples": null, + "displayName": null, + "headers": [], + "docs": null + }, + "error_types/object:ObjectWithOptionalFieldError": { + "name": { + "name": { + "originalName": "ObjectWithOptionalFieldError", + "camelCase": { + "unsafeName": "objectWithOptionalFieldError", + "safeName": "objectWithOptionalFieldError" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field_error", + "safeName": "object_with_optional_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD_ERROR", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalFieldError", + "safeName": "ObjectWithOptionalFieldError" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "errorId": "error_types/object:ObjectWithOptionalFieldError" + }, + "discriminantValue": { + "name": { + "originalName": "ObjectWithOptionalFieldError", + "camelCase": { + "unsafeName": "objectWithOptionalFieldError", + "safeName": "objectWithOptionalFieldError" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field_error", + "safeName": "object_with_optional_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD_ERROR", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalFieldError", + "safeName": "ObjectWithOptionalFieldError" + } + }, + "wireValue": "ObjectWithOptionalFieldError" + }, + "statusCode": 400, + "isWildcardStatusCode": null, + "type": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "examples": [], + "v2Examples": null, + "displayName": null, + "headers": [], + "docs": null + }, + "error_types/object:ObjectWithRequiredFieldError": { + "name": { + "name": { + "originalName": "ObjectWithRequiredFieldError", + "camelCase": { + "unsafeName": "objectWithRequiredFieldError", + "safeName": "objectWithRequiredFieldError" + }, + "snakeCase": { + "unsafeName": "object_with_required_field_error", + "safeName": "object_with_required_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD_ERROR", + "safeName": "OBJECT_WITH_REQUIRED_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredFieldError", + "safeName": "ObjectWithRequiredFieldError" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "errorId": "error_types/object:ObjectWithRequiredFieldError" + }, + "discriminantValue": { + "name": { + "originalName": "ObjectWithRequiredFieldError", + "camelCase": { + "unsafeName": "objectWithRequiredFieldError", + "safeName": "objectWithRequiredFieldError" + }, + "snakeCase": { + "unsafeName": "object_with_required_field_error", + "safeName": "object_with_required_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD_ERROR", + "safeName": "OBJECT_WITH_REQUIRED_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredFieldError", + "safeName": "ObjectWithRequiredFieldError" + } + }, + "wireValue": "ObjectWithRequiredFieldError" + }, + "statusCode": 400, + "isWildcardStatusCode": null, + "type": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "examples": [], + "v2Examples": null, + "displayName": null, + "headers": [], + "docs": null + }, + "error_types/object:NestedObjectWithOptionalFieldError": { + "name": { + "name": { + "originalName": "NestedObjectWithOptionalFieldError", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalFieldError", + "safeName": "nestedObjectWithOptionalFieldError" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field_error", + "safeName": "nested_object_with_optional_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD_ERROR", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalFieldError", + "safeName": "NestedObjectWithOptionalFieldError" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "errorId": "error_types/object:NestedObjectWithOptionalFieldError" + }, + "discriminantValue": { + "name": { + "originalName": "NestedObjectWithOptionalFieldError", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalFieldError", + "safeName": "nestedObjectWithOptionalFieldError" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field_error", + "safeName": "nested_object_with_optional_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD_ERROR", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalFieldError", + "safeName": "NestedObjectWithOptionalFieldError" + } + }, + "wireValue": "NestedObjectWithOptionalFieldError" + }, + "statusCode": 400, + "isWildcardStatusCode": null, + "type": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField", + "default": null, + "inline": null + }, + "examples": [], + "v2Examples": null, + "displayName": null, + "headers": [], + "docs": null + }, + "error_types/object:NestedObjectWithRequiredFieldError": { + "name": { + "name": { + "originalName": "NestedObjectWithRequiredFieldError", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredFieldError", + "safeName": "nestedObjectWithRequiredFieldError" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field_error", + "safeName": "nested_object_with_required_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD_ERROR", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredFieldError", + "safeName": "NestedObjectWithRequiredFieldError" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "errorId": "error_types/object:NestedObjectWithRequiredFieldError" + }, + "discriminantValue": { + "name": { + "originalName": "NestedObjectWithRequiredFieldError", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredFieldError", + "safeName": "nestedObjectWithRequiredFieldError" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field_error", + "safeName": "nested_object_with_required_field_error" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD_ERROR", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD_ERROR" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredFieldError", + "safeName": "NestedObjectWithRequiredFieldError" + } + }, + "wireValue": "NestedObjectWithRequiredFieldError" + }, + "statusCode": 400, + "isWildcardStatusCode": null, + "type": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + }, + "examples": [], + "v2Examples": null, + "displayName": null, + "headers": [], + "docs": null + }, + "error_types/union:ErrorWithUnionBody": { + "name": { + "name": { + "originalName": "ErrorWithUnionBody", + "camelCase": { + "unsafeName": "errorWithUnionBody", + "safeName": "errorWithUnionBody" + }, + "snakeCase": { + "unsafeName": "error_with_union_body", + "safeName": "error_with_union_body" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_WITH_UNION_BODY", + "safeName": "ERROR_WITH_UNION_BODY" + }, + "pascalCase": { + "unsafeName": "ErrorWithUnionBody", + "safeName": "ErrorWithUnionBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "errorId": "error_types/union:ErrorWithUnionBody" + }, + "discriminantValue": { + "name": { + "originalName": "ErrorWithUnionBody", + "camelCase": { + "unsafeName": "errorWithUnionBody", + "safeName": "errorWithUnionBody" + }, + "snakeCase": { + "unsafeName": "error_with_union_body", + "safeName": "error_with_union_body" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_WITH_UNION_BODY", + "safeName": "ERROR_WITH_UNION_BODY" + }, + "pascalCase": { + "unsafeName": "ErrorWithUnionBody", + "safeName": "ErrorWithUnionBody" + } + }, + "wireValue": "ErrorWithUnionBody" + }, + "statusCode": 400, + "isWildcardStatusCode": null, + "type": { + "_type": "named", + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Animal", + "default": null, + "inline": null + }, + "examples": [], + "v2Examples": null, + "displayName": null, + "headers": [], + "docs": null + } + }, + "services": { + "service_endpoints/container": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/container", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/container.getAndReturnListOfPrimitives", + "name": { + "originalName": "getAndReturnListOfPrimitives", + "camelCase": { + "unsafeName": "getAndReturnListOfPrimitives", + "safeName": "getAndReturnListOfPrimitives" + }, + "snakeCase": { + "unsafeName": "get_and_return_list_of_primitives", + "safeName": "get_and_return_list_of_primitives" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LIST_OF_PRIMITIVES", + "safeName": "GET_AND_RETURN_LIST_OF_PRIMITIVES" + }, + "pascalCase": { + "unsafeName": "GetAndReturnListOfPrimitives", + "safeName": "GetAndReturnListOfPrimitives" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/list-of-primitives", + "parts": [] + }, + "fullPath": { + "head": "/container/list-of-primitives", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "464113fd", + "url": "/container/list-of-primitives", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string", + "string" + ] + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string", + "string" + ] + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/container.getAndReturnListOfObjects", + "name": { + "originalName": "getAndReturnListOfObjects", + "camelCase": { + "unsafeName": "getAndReturnListOfObjects", + "safeName": "getAndReturnListOfObjects" + }, + "snakeCase": { + "unsafeName": "get_and_return_list_of_objects", + "safeName": "get_and_return_list_of_objects" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LIST_OF_OBJECTS", + "safeName": "GET_AND_RETURN_LIST_OF_OBJECTS" + }, + "pascalCase": { + "unsafeName": "GetAndReturnListOfObjects", + "safeName": "GetAndReturnListOfObjects" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/list-of-objects", + "parts": [] + }, + "fullPath": { + "head": "/container/list-of-objects", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "e6cf82d1", + "url": "/container/list-of-objects", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + }, + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "string": "string" + }, + { + "string": "string" + } + ] + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + }, + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "string": "string" + }, + { + "string": "string" + } + ] + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/container.getAndReturnSetOfPrimitives", + "name": { + "originalName": "getAndReturnSetOfPrimitives", + "camelCase": { + "unsafeName": "getAndReturnSetOfPrimitives", + "safeName": "getAndReturnSetOfPrimitives" + }, + "snakeCase": { + "unsafeName": "get_and_return_set_of_primitives", + "safeName": "get_and_return_set_of_primitives" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_SET_OF_PRIMITIVES", + "safeName": "GET_AND_RETURN_SET_OF_PRIMITIVES" + }, + "pascalCase": { + "unsafeName": "GetAndReturnSetOfPrimitives", + "safeName": "GetAndReturnSetOfPrimitives" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/set-of-primitives", + "parts": [] + }, + "fullPath": { + "head": "/container/set-of-primitives", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "ece867d1", + "url": "/container/set-of-primitives", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string" + ] + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "string" + ] + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/container.getAndReturnSetOfObjects", + "name": { + "originalName": "getAndReturnSetOfObjects", + "camelCase": { + "unsafeName": "getAndReturnSetOfObjects", + "safeName": "getAndReturnSetOfObjects" + }, + "snakeCase": { + "unsafeName": "get_and_return_set_of_objects", + "safeName": "get_and_return_set_of_objects" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_SET_OF_OBJECTS", + "safeName": "GET_AND_RETURN_SET_OF_OBJECTS" + }, + "pascalCase": { + "unsafeName": "GetAndReturnSetOfObjects", + "safeName": "GetAndReturnSetOfObjects" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/set-of-objects", + "parts": [] + }, + "fullPath": { + "head": "/container/set-of-objects", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "73267b8d", + "url": "/container/set-of-objects", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "string": "string" + } + ] + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "string": "string" + } + ] + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/container.getAndReturnMapPrimToPrim", + "name": { + "originalName": "getAndReturnMapPrimToPrim", + "camelCase": { + "unsafeName": "getAndReturnMapPrimToPrim", + "safeName": "getAndReturnMapPrimToPrim" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_prim_to_prim", + "safeName": "get_and_return_map_prim_to_prim" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_PRIM_TO_PRIM", + "safeName": "GET_AND_RETURN_MAP_PRIM_TO_PRIM" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapPrimToPrim", + "safeName": "GetAndReturnMapPrimToPrim" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/map-prim-to-prim", + "parts": [] + }, + "fullPath": { + "head": "/container/map-prim-to-prim", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "63c1f1a9", + "url": "/container/map-prim-to-prim", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "string": "string" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "string": "string" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/container.getAndReturnMapOfPrimToObject", + "name": { + "originalName": "getAndReturnMapOfPrimToObject", + "camelCase": { + "unsafeName": "getAndReturnMapOfPrimToObject", + "safeName": "getAndReturnMapOfPrimToObject" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_of_prim_to_object", + "safeName": "get_and_return_map_of_prim_to_object" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_OBJECT", + "safeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_OBJECT" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapOfPrimToObject", + "safeName": "GetAndReturnMapOfPrimToObject" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/map-prim-to-object", + "parts": [] + }, + "fullPath": { + "head": "/container/map-prim-to-object", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b15aa177", + "url": "/container/map-prim-to-object", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": { + "string": { + "string": "string" + } + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": { + "string": { + "string": "string" + } + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/container.getAndReturnMapOfPrimToUndiscriminatedUnion", + "name": { + "originalName": "getAndReturnMapOfPrimToUndiscriminatedUnion", + "camelCase": { + "unsafeName": "getAndReturnMapOfPrimToUndiscriminatedUnion", + "safeName": "getAndReturnMapOfPrimToUndiscriminatedUnion" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_of_prim_to_undiscriminated_union", + "safeName": "get_and_return_map_of_prim_to_undiscriminated_union" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_UNDISCRIMINATED_UNION", + "safeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_UNDISCRIMINATED_UNION" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapOfPrimToUndiscriminatedUnion", + "safeName": "GetAndReturnMapOfPrimToUndiscriminatedUnion" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/map-prim-to-union", + "parts": [] + }, + "fullPath": { + "head": "/container/map-prim-to-union", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType", + "default": null, + "inline": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "2ea4a02d", + "url": "/container/map-prim-to-union", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "undiscriminatedUnion", + "index": 0, + "singleUnionType": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + } + }, + "typeName": { + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType" + } + }, + "jsonExample": 1.1 + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType", + "default": null, + "inline": null + } + } + }, + "jsonExample": { + "string": 1.1 + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "undiscriminatedUnion", + "index": 0, + "singleUnionType": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + } + }, + "typeName": { + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType" + } + }, + "jsonExample": 1.1 + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:MixedType", + "default": null, + "inline": null + } + } + }, + "jsonExample": { + "string": 1.1 + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/container.getAndReturnOptional", + "name": { + "originalName": "getAndReturnOptional", + "camelCase": { + "unsafeName": "getAndReturnOptional", + "safeName": "getAndReturnOptional" + }, + "snakeCase": { + "unsafeName": "get_and_return_optional", + "safeName": "get_and_return_optional" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_OPTIONAL", + "safeName": "GET_AND_RETURN_OPTIONAL" + }, + "pascalCase": { + "unsafeName": "GetAndReturnOptional", + "safeName": "GetAndReturnOptional" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/opt-objects", + "parts": [] + }, + "fullPath": { + "head": "/container/opt-objects", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "244c6060", + "url": "/container/opt-objects", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": { + "string": "string" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_endpoints/content-type": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/foo", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/content-type.postJsonPatchContentType", + "name": { + "originalName": "postJsonPatchContentType", + "camelCase": { + "unsafeName": "postJSONPatchContentType", + "safeName": "postJSONPatchContentType" + }, + "snakeCase": { + "unsafeName": "post_json_patch_content_type", + "safeName": "post_json_patch_content_type" + }, + "screamingSnakeCase": { + "unsafeName": "POST_JSON_PATCH_CONTENT_TYPE", + "safeName": "POST_JSON_PATCH_CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "PostJSONPatchContentType", + "safeName": "PostJSONPatchContentType" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/bar", + "parts": [] + }, + "fullPath": { + "head": "/foo/bar", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": "application/json-patch+json", + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "5df8acde", + "url": "/foo/bar", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/content-type.postJsonPatchContentWithCharsetType", + "name": { + "originalName": "postJsonPatchContentWithCharsetType", + "camelCase": { + "unsafeName": "postJSONPatchContentWithCharsetType", + "safeName": "postJSONPatchContentWithCharsetType" + }, + "snakeCase": { + "unsafeName": "post_json_patch_content_with_charset_type", + "safeName": "post_json_patch_content_with_charset_type" + }, + "screamingSnakeCase": { + "unsafeName": "POST_JSON_PATCH_CONTENT_WITH_CHARSET_TYPE", + "safeName": "POST_JSON_PATCH_CONTENT_WITH_CHARSET_TYPE" + }, + "pascalCase": { + "unsafeName": "PostJSONPatchContentWithCharsetType", + "safeName": "PostJSONPatchContentWithCharsetType" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/baz", + "parts": [] + }, + "fullPath": { + "head": "/foo/baz", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": "application/json-patch+json; charset=utf-8", + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "5df8acde", + "url": "/foo/baz", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_endpoints/duplicate-names-a": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/duplicate-names-a", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/duplicate-names-a.create", + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/duplicate-names-a", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "inlinedRequestBody", + "name": { + "originalName": "CreateRequestA", + "camelCase": { + "unsafeName": "createRequestA", + "safeName": "createRequestA" + }, + "snakeCase": { + "unsafeName": "create_request_a", + "safeName": "create_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_A", + "safeName": "CREATE_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "CreateRequestA", + "safeName": "CreateRequestA" + } + }, + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "value", + "camelCase": { + "unsafeName": "value", + "safeName": "value" + }, + "snakeCase": { + "unsafeName": "value", + "safeName": "value" + }, + "screamingSnakeCase": { + "unsafeName": "VALUE", + "safeName": "VALUE" + }, + "pascalCase": { + "unsafeName": "Value", + "safeName": "Value" + } + }, + "wireValue": "value" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], + "docs": null, + "v2Examples": null, + "contentType": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "CreateRequestA", + "camelCase": { + "unsafeName": "createRequestA", + "safeName": "createRequestA" + }, + "snakeCase": { + "unsafeName": "create_request_a", + "safeName": "create_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_A", + "safeName": "CREATE_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "CreateRequestA", + "safeName": "CreateRequestA" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "13c963d8", + "url": "/duplicate-names-a", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "name" + } + } + }, + "jsonExample": "name" + } + }, + { + "name": { + "name": { + "originalName": "value", + "camelCase": { + "unsafeName": "value", + "safeName": "value" + }, + "snakeCase": { + "unsafeName": "value", + "safeName": "value" + }, + "screamingSnakeCase": { + "unsafeName": "VALUE", + "safeName": "VALUE" + }, + "pascalCase": { + "unsafeName": "Value", + "safeName": "Value" + } + }, + "wireValue": "value" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + } + ], + "extraProperties": null, + "jsonExample": { + "name": "name", + "value": 1 + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "Create endpoint for service A" + }, + { + "id": "endpoint_endpoints/duplicate-names-a.get", + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/duplicate-names-a/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "filter", + "camelCase": { + "unsafeName": "filter", + "safeName": "filter" + }, + "snakeCase": { + "unsafeName": "filter", + "safeName": "filter" + }, + "screamingSnakeCase": { + "unsafeName": "FILTER", + "safeName": "FILTER" + }, + "pascalCase": { + "unsafeName": "Filter", + "safeName": "Filter" + } + }, + "wireValue": "filter" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "GetRequestA", + "camelCase": { + "unsafeName": "getRequestA", + "safeName": "getRequestA" + }, + "snakeCase": { + "unsafeName": "get_request_a", + "safeName": "get_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_A", + "safeName": "GET_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "GetRequestA", + "safeName": "GetRequestA" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": true, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "1c54673f", + "url": "/duplicate-names-a/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "Get endpoint for service A" + }, + { + "id": "endpoint_endpoints/duplicate-names-a.list", + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/duplicate-names-a", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "page", + "camelCase": { + "unsafeName": "page", + "safeName": "page" + }, + "snakeCase": { + "unsafeName": "page", + "safeName": "page" + }, + "screamingSnakeCase": { + "unsafeName": "PAGE", + "safeName": "PAGE" + }, + "pascalCase": { + "unsafeName": "Page", + "safeName": "Page" + } + }, + "wireValue": "page" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ListRequestA", + "camelCase": { + "unsafeName": "listRequestA", + "safeName": "listRequestA" + }, + "snakeCase": { + "unsafeName": "list_request_a", + "safeName": "list_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_A", + "safeName": "LIST_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "ListRequestA", + "safeName": "ListRequestA" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b8b5a106", + "url": "/duplicate-names-a", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "List endpoint for service A" + } + ], + "audiences": null + }, + "service_endpoints/duplicate-names-b": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/duplicate-names-b", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/duplicate-names-b.create", + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/duplicate-names-b", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "inlinedRequestBody", + "name": { + "originalName": "CreateRequestB", + "camelCase": { + "unsafeName": "createRequestB", + "safeName": "createRequestB" + }, + "snakeCase": { + "unsafeName": "create_request_b", + "safeName": "create_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_B", + "safeName": "CREATE_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "CreateRequestB", + "safeName": "CreateRequestB" + } + }, + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "description", + "camelCase": { + "unsafeName": "description", + "safeName": "description" + }, + "snakeCase": { + "unsafeName": "description", + "safeName": "description" + }, + "screamingSnakeCase": { + "unsafeName": "DESCRIPTION", + "safeName": "DESCRIPTION" + }, + "pascalCase": { + "unsafeName": "Description", + "safeName": "Description" + } + }, + "wireValue": "description" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "count", + "camelCase": { + "unsafeName": "count", + "safeName": "count" + }, + "snakeCase": { + "unsafeName": "count", + "safeName": "count" + }, + "screamingSnakeCase": { + "unsafeName": "COUNT", + "safeName": "COUNT" + }, + "pascalCase": { + "unsafeName": "Count", + "safeName": "Count" + } + }, + "wireValue": "count" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], + "docs": null, + "v2Examples": null, + "contentType": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "CreateRequestB", + "camelCase": { + "unsafeName": "createRequestB", + "safeName": "createRequestB" + }, + "snakeCase": { + "unsafeName": "create_request_b", + "safeName": "create_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_B", + "safeName": "CREATE_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "CreateRequestB", + "safeName": "CreateRequestB" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "e09d0cf8", + "url": "/duplicate-names-b", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": { + "name": { + "originalName": "description", + "camelCase": { + "unsafeName": "description", + "safeName": "description" + }, + "snakeCase": { + "unsafeName": "description", + "safeName": "description" + }, + "screamingSnakeCase": { + "unsafeName": "DESCRIPTION", + "safeName": "DESCRIPTION" + }, + "pascalCase": { + "unsafeName": "Description", + "safeName": "Description" + } + }, + "wireValue": "description" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "description" + } + } + }, + "jsonExample": "description" + } + }, + { + "name": { + "name": { + "originalName": "count", + "camelCase": { + "unsafeName": "count", + "safeName": "count" + }, + "snakeCase": { + "unsafeName": "count", + "safeName": "count" + }, + "screamingSnakeCase": { + "unsafeName": "COUNT", + "safeName": "COUNT" + }, + "pascalCase": { + "unsafeName": "Count", + "safeName": "Count" + } + }, + "wireValue": "count" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + } + ], + "extraProperties": null, + "jsonExample": { + "description": "description", + "count": 1 + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "Create endpoint for service B" + }, + { + "id": "endpoint_endpoints/duplicate-names-b.get", + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/duplicate-names-b/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "expand", + "camelCase": { + "unsafeName": "expand", + "safeName": "expand" + }, + "snakeCase": { + "unsafeName": "expand", + "safeName": "expand" + }, + "screamingSnakeCase": { + "unsafeName": "EXPAND", + "safeName": "EXPAND" + }, + "pascalCase": { + "unsafeName": "Expand", + "safeName": "Expand" + } + }, + "wireValue": "expand" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "GetRequestB", + "camelCase": { + "unsafeName": "getRequestB", + "safeName": "getRequestB" + }, + "snakeCase": { + "unsafeName": "get_request_b", + "safeName": "get_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_B", + "safeName": "GET_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "GetRequestB", + "safeName": "GetRequestB" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": true, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "1c54673f", + "url": "/duplicate-names-b/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "Get endpoint for service B" + }, + { + "id": "endpoint_endpoints/duplicate-names-b.list", + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/duplicate-names-b", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "cursor", + "camelCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "snakeCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "screamingSnakeCase": { + "unsafeName": "CURSOR", + "safeName": "CURSOR" + }, + "pascalCase": { + "unsafeName": "Cursor", + "safeName": "Cursor" + } + }, + "wireValue": "cursor" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "size", + "camelCase": { + "unsafeName": "size", + "safeName": "size" + }, + "snakeCase": { + "unsafeName": "size", + "safeName": "size" + }, + "screamingSnakeCase": { + "unsafeName": "SIZE", + "safeName": "SIZE" + }, + "pascalCase": { + "unsafeName": "Size", + "safeName": "Size" + } + }, + "wireValue": "size" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ListRequestB", + "camelCase": { + "unsafeName": "listRequestB", + "safeName": "listRequestB" + }, + "snakeCase": { + "unsafeName": "list_request_b", + "safeName": "list_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_B", + "safeName": "LIST_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "ListRequestB", + "safeName": "ListRequestB" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b8b5a106", + "url": "/duplicate-names-b", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "List endpoint for service B" + } + ], + "audiences": null + }, + "service_endpoints/duplicate-names-c": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/duplicate-names-c", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/duplicate-names-c.create", + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/duplicate-names-c", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "inlinedRequestBody", + "name": { + "originalName": "CreateRequestC", + "camelCase": { + "unsafeName": "createRequestC", + "safeName": "createRequestC" + }, + "snakeCase": { + "unsafeName": "create_request_c", + "safeName": "create_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_C", + "safeName": "CREATE_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "CreateRequestC", + "safeName": "CreateRequestC" + } + }, + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "label", + "camelCase": { + "unsafeName": "label", + "safeName": "label" + }, + "snakeCase": { + "unsafeName": "label", + "safeName": "label" + }, + "screamingSnakeCase": { + "unsafeName": "LABEL", + "safeName": "LABEL" + }, + "pascalCase": { + "unsafeName": "Label", + "safeName": "Label" + } + }, + "wireValue": "label" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "priority", + "camelCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "snakeCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "screamingSnakeCase": { + "unsafeName": "PRIORITY", + "safeName": "PRIORITY" + }, + "pascalCase": { + "unsafeName": "Priority", + "safeName": "Priority" + } + }, + "wireValue": "priority" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], + "docs": null, + "v2Examples": null, + "contentType": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "CreateRequestC", + "camelCase": { + "unsafeName": "createRequestC", + "safeName": "createRequestC" + }, + "snakeCase": { + "unsafeName": "create_request_c", + "safeName": "create_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_C", + "safeName": "CREATE_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "CreateRequestC", + "safeName": "CreateRequestC" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "9cd77cf9", + "url": "/duplicate-names-c", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": { + "name": { + "originalName": "label", + "camelCase": { + "unsafeName": "label", + "safeName": "label" + }, + "snakeCase": { + "unsafeName": "label", + "safeName": "label" + }, + "screamingSnakeCase": { + "unsafeName": "LABEL", + "safeName": "LABEL" + }, + "pascalCase": { + "unsafeName": "Label", + "safeName": "Label" + } + }, + "wireValue": "label" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "label" + } + } + }, + "jsonExample": "label" + } + }, + { + "name": { + "name": { + "originalName": "priority", + "camelCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "snakeCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "screamingSnakeCase": { + "unsafeName": "PRIORITY", + "safeName": "PRIORITY" + }, + "pascalCase": { + "unsafeName": "Priority", + "safeName": "Priority" + } + }, + "wireValue": "priority" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + } + ], + "extraProperties": null, + "jsonExample": { + "label": "label", + "priority": 1 + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "Create endpoint for service C" + }, + { + "id": "endpoint_endpoints/duplicate-names-c.get", + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/duplicate-names-c/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "verbose", + "camelCase": { + "unsafeName": "verbose", + "safeName": "verbose" + }, + "snakeCase": { + "unsafeName": "verbose", + "safeName": "verbose" + }, + "screamingSnakeCase": { + "unsafeName": "VERBOSE", + "safeName": "VERBOSE" + }, + "pascalCase": { + "unsafeName": "Verbose", + "safeName": "Verbose" + } + }, + "wireValue": "verbose" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "GetRequestC", + "camelCase": { + "unsafeName": "getRequestC", + "safeName": "getRequestC" + }, + "snakeCase": { + "unsafeName": "get_request_c", + "safeName": "get_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_C", + "safeName": "GET_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "GetRequestC", + "safeName": "GetRequestC" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": true, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "1c54673f", + "url": "/duplicate-names-c/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "Get endpoint for service C" + }, + { + "id": "endpoint_endpoints/duplicate-names-c.list", + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/duplicate-names-c", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "offset", + "camelCase": { + "unsafeName": "offset", + "safeName": "offset" + }, + "snakeCase": { + "unsafeName": "offset", + "safeName": "offset" + }, + "screamingSnakeCase": { + "unsafeName": "OFFSET", + "safeName": "OFFSET" + }, + "pascalCase": { + "unsafeName": "Offset", + "safeName": "Offset" + } + }, + "wireValue": "offset" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "count", + "camelCase": { + "unsafeName": "count", + "safeName": "count" + }, + "snakeCase": { + "unsafeName": "count", + "safeName": "count" + }, + "screamingSnakeCase": { + "unsafeName": "COUNT", + "safeName": "COUNT" + }, + "pascalCase": { + "unsafeName": "Count", + "safeName": "Count" + } + }, + "wireValue": "count" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ListRequestC", + "camelCase": { + "unsafeName": "listRequestC", + "safeName": "listRequestC" + }, + "snakeCase": { + "unsafeName": "list_request_c", + "safeName": "list_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_C", + "safeName": "LIST_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "ListRequestC", + "safeName": "ListRequestC" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b8b5a106", + "url": "/duplicate-names-c", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "List endpoint for service C" + } + ], + "audiences": null + }, + "service_endpoints/enum": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/enum", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/enum.getAndReturnEnum", + "name": { + "originalName": "getAndReturnEnum", + "camelCase": { + "unsafeName": "getAndReturnEnum", + "safeName": "getAndReturnEnum" + }, + "snakeCase": { + "unsafeName": "get_and_return_enum", + "safeName": "get_and_return_enum" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_ENUM", + "safeName": "GET_AND_RETURN_ENUM" + }, + "pascalCase": { + "unsafeName": "GetAndReturnEnum", + "safeName": "GetAndReturnEnum" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/enum", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "displayName": null, + "typeId": "type_types/enum:WeatherReport", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "displayName": null, + "typeId": "type_types/enum:WeatherReport", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "displayName": null, + "typeId": "type_types/enum:WeatherReport", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d03750c1", + "url": "/enum", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "enum", + "value": { + "name": { + "originalName": "SUNNY", + "camelCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "snakeCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "screamingSnakeCase": { + "unsafeName": "SUNNY", + "safeName": "SUNNY" + }, + "pascalCase": { + "unsafeName": "Sunny", + "safeName": "Sunny" + } + }, + "wireValue": "SUNNY" + } + }, + "typeName": { + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "displayName": null, + "typeId": "type_types/enum:WeatherReport" + } + }, + "jsonExample": "SUNNY" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "enum", + "value": { + "name": { + "originalName": "SUNNY", + "camelCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "snakeCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "screamingSnakeCase": { + "unsafeName": "SUNNY", + "safeName": "SUNNY" + }, + "pascalCase": { + "unsafeName": "Sunny", + "safeName": "Sunny" + } + }, + "wireValue": "SUNNY" + } + }, + "typeName": { + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "displayName": null, + "typeId": "type_types/enum:WeatherReport" + } + }, + "jsonExample": "SUNNY" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_endpoints/http-methods": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/http-methods", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/http-methods.testGet", + "name": { + "originalName": "testGet", + "camelCase": { + "unsafeName": "testGet", + "safeName": "testGet" + }, + "snakeCase": { + "unsafeName": "test_get", + "safeName": "test_get" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_GET", + "safeName": "TEST_GET" + }, + "pascalCase": { + "unsafeName": "TestGet", + "safeName": "TestGet" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/http-methods/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "a52a5ddc", + "url": "/http-methods/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/http-methods.testPost", + "name": { + "originalName": "testPost", + "camelCase": { + "unsafeName": "testPost", + "safeName": "testPost" + }, + "snakeCase": { + "unsafeName": "test_post", + "safeName": "test_post" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_POST", + "safeName": "TEST_POST" + }, + "pascalCase": { + "unsafeName": "TestPost", + "safeName": "TestPost" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/http-methods", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b99a14e3", + "url": "/http-methods", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": { + "status": "DEPRECATED", + "message": null + }, + "docs": null + }, + { + "id": "endpoint_endpoints/http-methods.testPut", + "name": { + "originalName": "testPut", + "camelCase": { + "unsafeName": "testPut", + "safeName": "testPut" + }, + "snakeCase": { + "unsafeName": "test_put", + "safeName": "test_put" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_PUT", + "safeName": "TEST_PUT" + }, + "pascalCase": { + "unsafeName": "TestPut", + "safeName": "TestPut" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "PUT", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/http-methods/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "c1254a9c", + "url": "/http-methods/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": { + "status": "DEPRECATED", + "message": "Use testPatch instead." + }, + "docs": null + }, + { + "id": "endpoint_endpoints/http-methods.testPatch", + "name": { + "originalName": "testPatch", + "camelCase": { + "unsafeName": "testPatch", + "safeName": "testPatch" + }, + "snakeCase": { + "unsafeName": "test_patch", + "safeName": "test_patch" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_PATCH", + "safeName": "TEST_PATCH" + }, + "pascalCase": { + "unsafeName": "TestPatch", + "safeName": "TestPatch" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "PATCH", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/http-methods/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "cce429b4", + "url": "/http-methods/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": { + "status": "PRE_RELEASE", + "message": null + }, + "docs": null + }, + { + "id": "endpoint_endpoints/http-methods.testDelete", + "name": { + "originalName": "testDelete", + "camelCase": { + "unsafeName": "testDelete", + "safeName": "testDelete" + }, + "snakeCase": { + "unsafeName": "test_delete", + "safeName": "test_delete" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_DELETE", + "safeName": "TEST_DELETE" + }, + "pascalCase": { + "unsafeName": "TestDelete", + "safeName": "TestDelete" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "DELETE", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/http-methods/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "3bd79b6d", + "url": "/http-methods/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": { + "status": "IN_DEVELOPMENT", + "message": null + }, + "docs": null + } + ], + "audiences": null + }, + "service_endpoints/object": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/object", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/object.getAndReturnWithOptionalField", + "name": { + "originalName": "getAndReturnWithOptionalField", + "camelCase": { + "unsafeName": "getAndReturnWithOptionalField", + "safeName": "getAndReturnWithOptionalField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_optional_field", + "safeName": "get_and_return_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_OPTIONAL_FIELD", + "safeName": "GET_AND_RETURN_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithOptionalField", + "safeName": "GetAndReturnWithOptionalField" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-with-optional-field", + "parts": [] + }, + "fullPath": { + "head": "/object/get-and-return-with-optional-field", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "ee6f66e3", + "url": "/object/get-and-return-with-optional-field", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/object.getAndReturnWithRequiredField", + "name": { + "originalName": "getAndReturnWithRequiredField", + "camelCase": { + "unsafeName": "getAndReturnWithRequiredField", + "safeName": "getAndReturnWithRequiredField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_required_field", + "safeName": "get_and_return_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_REQUIRED_FIELD", + "safeName": "GET_AND_RETURN_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithRequiredField", + "safeName": "GetAndReturnWithRequiredField" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-with-required-field", + "parts": [] + }, + "fullPath": { + "head": "/object/get-and-return-with-required-field", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d214d2bf", + "url": "/object/get-and-return-with-required-field", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/object.getAndReturnWithMapOfMap", + "name": { + "originalName": "getAndReturnWithMapOfMap", + "camelCase": { + "unsafeName": "getAndReturnWithMapOfMap", + "safeName": "getAndReturnWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_map_of_map", + "safeName": "get_and_return_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_MAP_OF_MAP", + "safeName": "GET_AND_RETURN_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithMapOfMap", + "safeName": "GetAndReturnWithMapOfMap" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-with-map-of-map", + "parts": [] + }, + "fullPath": { + "head": "/object/get-and-return-with-map-of-map", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "93160029", + "url": "/object/get-and-return-with-map-of-map", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "map": "map" + } + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "map": { + "map": "map" + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap" + } + }, + "jsonExample": { + "map": { + "map": { + "map": "map" + } + } + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "map": "map" + } + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "map": { + "map": "map" + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithMapOfMap" + } + }, + "jsonExample": { + "map": { + "map": { + "map": "map" + } + } + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/object.getAndReturnNestedWithOptionalField", + "name": { + "originalName": "getAndReturnNestedWithOptionalField", + "camelCase": { + "unsafeName": "getAndReturnNestedWithOptionalField", + "safeName": "getAndReturnNestedWithOptionalField" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_optional_field", + "safeName": "get_and_return_nested_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_OPTIONAL_FIELD", + "safeName": "GET_AND_RETURN_NESTED_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithOptionalField", + "safeName": "GetAndReturnNestedWithOptionalField" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-nested-with-optional-field", + "parts": [] + }, + "fullPath": { + "head": "/object/get-and-return-nested-with-optional-field", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "7c8b88f1", + "url": "/object/get-and-return-nested-with-optional-field", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + } + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "NestedObject": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/object.getAndReturnNestedWithRequiredField", + "name": { + "originalName": "getAndReturnNestedWithRequiredField", + "camelCase": { + "unsafeName": "getAndReturnNestedWithRequiredField", + "safeName": "getAndReturnNestedWithRequiredField" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_required_field", + "safeName": "get_and_return_nested_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD", + "safeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithRequiredField", + "safeName": "GetAndReturnNestedWithRequiredField" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-nested-with-required-field/", + "parts": [ + { + "pathParameter": "string", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/object/get-and-return-nested-with-required-field/", + "parts": [ + { + "pathParameter": "string", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "81c0556a", + "url": "/object/get-and-return-nested-with-required-field/string", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string", + "NestedObject": {} + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string", + "NestedObject": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/object.getAndReturnNestedWithRequiredFieldAsList", + "name": { + "originalName": "getAndReturnNestedWithRequiredFieldAsList", + "camelCase": { + "unsafeName": "getAndReturnNestedWithRequiredFieldAsList", + "safeName": "getAndReturnNestedWithRequiredFieldAsList" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_required_field_as_list", + "safeName": "get_and_return_nested_with_required_field_as_list" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD_AS_LIST", + "safeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD_AS_LIST" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithRequiredFieldAsList", + "safeName": "GetAndReturnNestedWithRequiredFieldAsList" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-nested-with-required-field-list", + "parts": [] + }, + "fullPath": { + "head": "/object/get-and-return-nested-with-required-field-list", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "2a9c431d", + "url": "/object/get-and-return-nested-with-required-field-list", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string", + "NestedObject": {} + } + }, + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string", + "NestedObject": {} + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "string": "string", + "NestedObject": {} + }, + { + "string": "string", + "NestedObject": {} + } + ] + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:NestedObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string", + "NestedObject": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/object.getAndReturnWithUnknownField", + "name": { + "originalName": "getAndReturnWithUnknownField", + "camelCase": { + "unsafeName": "getAndReturnWithUnknownField", + "safeName": "getAndReturnWithUnknownField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_unknown_field", + "safeName": "get_and_return_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_UNKNOWN_FIELD", + "safeName": "GET_AND_RETURN_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithUnknownField", + "safeName": "GetAndReturnWithUnknownField" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-with-unknown-field", + "parts": [] + }, + "fullPath": { + "head": "/object/get-and-return-with-unknown-field", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "BackslashExample", + "name": { + "originalName": "BackslashExample", + "camelCase": { + "unsafeName": "backslashExample", + "safeName": "backslashExample" + }, + "snakeCase": { + "unsafeName": "backslash_example", + "safeName": "backslash_example" + }, + "screamingSnakeCase": { + "unsafeName": "BACKSLASH_EXAMPLE", + "safeName": "BACKSLASH_EXAMPLE" + }, + "pascalCase": { + "unsafeName": "BackslashExample", + "safeName": "BackslashExample" + } + }, + "url": "/object/get-and-return-with-unknown-field", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "typeName": { + "typeId": "type_types/object:ObjectWithUnknownField", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "unknown" + }, + "value": { + "shape": { + "type": "unknown", + "unknown": { + "$ref": "https://example.com/schema" + } + }, + "jsonExample": { + "$ref": "https://example.com/schema" + } + }, + "originalTypeDeclaration": { + "typeId": "type_types/object:ObjectWithUnknownField", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "unknown": { + "$ref": "https://example.com/schema" + } + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_types/object:ObjectWithUnknownField", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "unknown" + }, + "value": { + "shape": { + "type": "unknown", + "unknown": { + "$ref": "https://example.com/schema" + } + }, + "jsonExample": { + "$ref": "https://example.com/schema" + } + }, + "originalTypeDeclaration": { + "typeId": "type_types/object:ObjectWithUnknownField", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "unknown": { + "$ref": "https://example.com/schema" + } + } + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "50f71a61", + "url": "/object/get-and-return-with-unknown-field", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "unknown" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField" + }, + "value": { + "shape": { + "type": "unknown", + "unknown": { + "key": "value" + } + }, + "jsonExample": { + "key": "value" + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField" + } + }, + "jsonExample": { + "unknown": { + "key": "value" + } + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "unknown" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField" + }, + "value": { + "shape": { + "type": "unknown", + "unknown": { + "key": "value" + } + }, + "jsonExample": { + "key": "value" + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithUnknownField" + } + }, + "jsonExample": { + "unknown": { + "key": "value" + } + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/object.getAndReturnWithDatetimeLikeString", + "name": { + "originalName": "getAndReturnWithDatetimeLikeString", + "camelCase": { + "unsafeName": "getAndReturnWithDatetimeLikeString", + "safeName": "getAndReturnWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_datetime_like_string", + "safeName": "get_and_return_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_DATETIME_LIKE_STRING", + "safeName": "GET_AND_RETURN_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithDatetimeLikeString", + "safeName": "GetAndReturnWithDatetimeLikeString" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/get-and-return-with-datetime-like-string", + "parts": [] + }, + "fullPath": { + "head": "/object/get-and-return-with-datetime-like-string", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "DatetimeLikeStringExample", + "name": { + "originalName": "DatetimeLikeStringExample", + "camelCase": { + "unsafeName": "datetimeLikeStringExample", + "safeName": "datetimeLikeStringExample" + }, + "snakeCase": { + "unsafeName": "datetime_like_string_example", + "safeName": "datetime_like_string_example" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING_EXAMPLE", + "safeName": "DATETIME_LIKE_STRING_EXAMPLE" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeStringExample", + "safeName": "DatetimeLikeStringExample" + } + }, + "url": "/object/get-and-return-with-datetime-like-string", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "typeName": { + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "datetimeLikeString", + "camelCase": { + "unsafeName": "datetimeLikeString", + "safeName": "datetimeLikeString" + }, + "snakeCase": { + "unsafeName": "datetime_like_string", + "safeName": "datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING", + "safeName": "DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeString", + "safeName": "DatetimeLikeString" + } + }, + "wireValue": "datetimeLikeString" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "2023-08-31T14:15:22Z" + } + } + }, + "jsonExample": "2023-08-31T14:15:22Z" + }, + "originalTypeDeclaration": { + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "displayName": null + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "actualDatetime", + "camelCase": { + "unsafeName": "actualDatetime", + "safeName": "actualDatetime" + }, + "snakeCase": { + "unsafeName": "actual_datetime", + "safeName": "actual_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "ACTUAL_DATETIME", + "safeName": "ACTUAL_DATETIME" + }, + "pascalCase": { + "unsafeName": "ActualDatetime", + "safeName": "ActualDatetime" + } + }, + "wireValue": "actualDatetime" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2023-08-31T14:15:22.000Z", + "raw": "2023-08-31T14:15:22Z" + } + }, + "jsonExample": "2023-08-31T14:15:22Z" + }, + "originalTypeDeclaration": { + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "datetimeLikeString": "2023-08-31T14:15:22Z", + "actualDatetime": "2023-08-31T14:15:22Z" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "datetimeLikeString", + "camelCase": { + "unsafeName": "datetimeLikeString", + "safeName": "datetimeLikeString" + }, + "snakeCase": { + "unsafeName": "datetime_like_string", + "safeName": "datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING", + "safeName": "DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeString", + "safeName": "DatetimeLikeString" + } + }, + "wireValue": "datetimeLikeString" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "2023-08-31T14:15:22Z" + } + } + }, + "jsonExample": "2023-08-31T14:15:22Z" + }, + "originalTypeDeclaration": { + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "displayName": null + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "actualDatetime", + "camelCase": { + "unsafeName": "actualDatetime", + "safeName": "actualDatetime" + }, + "snakeCase": { + "unsafeName": "actual_datetime", + "safeName": "actual_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "ACTUAL_DATETIME", + "safeName": "ACTUAL_DATETIME" + }, + "pascalCase": { + "unsafeName": "ActualDatetime", + "safeName": "ActualDatetime" + } + }, + "wireValue": "actualDatetime" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2023-08-31T14:15:22.000Z", + "raw": "2023-08-31T14:15:22Z" + } + }, + "jsonExample": "2023-08-31T14:15:22Z" + }, + "originalTypeDeclaration": { + "typeId": "type_types/object:ObjectWithDatetimeLikeString", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "datetimeLikeString": "2023-08-31T14:15:22Z", + "actualDatetime": "2023-08-31T14:15:22Z" + } + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "b7a1db1b", + "url": "/object/get-and-return-with-datetime-like-string", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "datetimeLikeString", + "camelCase": { + "unsafeName": "datetimeLikeString", + "safeName": "datetimeLikeString" + }, + "snakeCase": { + "unsafeName": "datetime_like_string", + "safeName": "datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING", + "safeName": "DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeString", + "safeName": "DatetimeLikeString" + } + }, + "wireValue": "datetimeLikeString" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "datetimeLikeString" + } + } + }, + "jsonExample": "datetimeLikeString" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "actualDatetime", + "camelCase": { + "unsafeName": "actualDatetime", + "safeName": "actualDatetime" + }, + "snakeCase": { + "unsafeName": "actual_datetime", + "safeName": "actual_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "ACTUAL_DATETIME", + "safeName": "ACTUAL_DATETIME" + }, + "pascalCase": { + "unsafeName": "ActualDatetime", + "safeName": "ActualDatetime" + } + }, + "wireValue": "actualDatetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString" + } + }, + "jsonExample": { + "datetimeLikeString": "datetimeLikeString", + "actualDatetime": "2024-01-15T09:30:00Z" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "datetimeLikeString", + "camelCase": { + "unsafeName": "datetimeLikeString", + "safeName": "datetimeLikeString" + }, + "snakeCase": { + "unsafeName": "datetime_like_string", + "safeName": "datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING", + "safeName": "DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeString", + "safeName": "DatetimeLikeString" + } + }, + "wireValue": "datetimeLikeString" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "datetimeLikeString" + } + } + }, + "jsonExample": "datetimeLikeString" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "actualDatetime", + "camelCase": { + "unsafeName": "actualDatetime", + "safeName": "actualDatetime" + }, + "snakeCase": { + "unsafeName": "actual_datetime", + "safeName": "actual_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "ACTUAL_DATETIME", + "safeName": "ACTUAL_DATETIME" + }, + "pascalCase": { + "unsafeName": "ActualDatetime", + "safeName": "ActualDatetime" + } + }, + "wireValue": "actualDatetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithDatetimeLikeString" + } + }, + "jsonExample": { + "datetimeLikeString": "datetimeLikeString", + "actualDatetime": "2024-01-15T09:30:00Z" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "Tests that string fields containing datetime-like values are NOT reformatted.\nThe datetimeLikeString field should preserve its exact value \"2023-08-31T14:15:22Z\"\nwithout being converted to \"2023-08-31T14:15:22.000Z\"." + } + ], + "audiences": null + }, + "service_endpoints/pagination": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/pagination", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/pagination.listItems", + "name": { + "originalName": "listItems", + "camelCase": { + "unsafeName": "listItems", + "safeName": "listItems" + }, + "snakeCase": { + "unsafeName": "list_items", + "safeName": "list_items" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_ITEMS", + "safeName": "LIST_ITEMS" + }, + "pascalCase": { + "unsafeName": "ListItems", + "safeName": "ListItems" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/pagination", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "cursor", + "camelCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "snakeCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "screamingSnakeCase": { + "unsafeName": "CURSOR", + "safeName": "CURSOR" + }, + "pascalCase": { + "unsafeName": "Cursor", + "safeName": "Cursor" + } + }, + "wireValue": "cursor" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": "The cursor for pagination" + }, + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": "Maximum number of items to return" + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ListItemsRequest", + "camelCase": { + "unsafeName": "listItemsRequest", + "safeName": "listItemsRequest" + }, + "snakeCase": { + "unsafeName": "list_items_request", + "safeName": "list_items_request" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_ITEMS_REQUEST", + "safeName": "LIST_ITEMS_REQUEST" + }, + "pascalCase": { + "unsafeName": "ListItemsRequest", + "safeName": "ListItemsRequest" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "PaginatedResponse", + "camelCase": { + "unsafeName": "paginatedResponse", + "safeName": "paginatedResponse" + }, + "snakeCase": { + "unsafeName": "paginated_response", + "safeName": "paginated_response" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATED_RESPONSE", + "safeName": "PAGINATED_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PaginatedResponse", + "safeName": "PaginatedResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/pagination:PaginatedResponse", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "fb0b4824", + "url": "/pagination", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "items", + "camelCase": { + "unsafeName": "items", + "safeName": "items" + }, + "snakeCase": { + "unsafeName": "items", + "safeName": "items" + }, + "screamingSnakeCase": { + "unsafeName": "ITEMS", + "safeName": "ITEMS" + }, + "pascalCase": { + "unsafeName": "Items", + "safeName": "Items" + } + }, + "wireValue": "items" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "PaginatedResponse", + "camelCase": { + "unsafeName": "paginatedResponse", + "safeName": "paginatedResponse" + }, + "snakeCase": { + "unsafeName": "paginated_response", + "safeName": "paginated_response" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATED_RESPONSE", + "safeName": "PAGINATED_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PaginatedResponse", + "safeName": "PaginatedResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/pagination:PaginatedResponse" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + }, + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField" + } + }, + "jsonExample": { + "string": "string" + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "string": "string" + }, + { + "string": "string" + } + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "next", + "camelCase": { + "unsafeName": "next", + "safeName": "next" + }, + "snakeCase": { + "unsafeName": "next", + "safeName": "next" + }, + "screamingSnakeCase": { + "unsafeName": "NEXT", + "safeName": "NEXT" + }, + "pascalCase": { + "unsafeName": "Next", + "safeName": "Next" + } + }, + "wireValue": "next" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "PaginatedResponse", + "camelCase": { + "unsafeName": "paginatedResponse", + "safeName": "paginatedResponse" + }, + "snakeCase": { + "unsafeName": "paginated_response", + "safeName": "paginated_response" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATED_RESPONSE", + "safeName": "PAGINATED_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PaginatedResponse", + "safeName": "PaginatedResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/pagination:PaginatedResponse" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "next" + } + } + }, + "jsonExample": "next" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "next" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "PaginatedResponse", + "camelCase": { + "unsafeName": "paginatedResponse", + "safeName": "paginatedResponse" + }, + "snakeCase": { + "unsafeName": "paginated_response", + "safeName": "paginated_response" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATED_RESPONSE", + "safeName": "PAGINATED_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PaginatedResponse", + "safeName": "PaginatedResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/pagination:PaginatedResponse" + } + }, + "jsonExample": { + "items": [ + { + "string": "string" + }, + { + "string": "string" + } + ], + "next": "next" + } + } + } + }, + "docs": null + } + } + ], + "pagination": { + "type": "cursor", + "page": { + "property": { + "type": "query", + "name": { + "name": { + "originalName": "cursor", + "camelCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "snakeCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "screamingSnakeCase": { + "unsafeName": "CURSOR", + "safeName": "CURSOR" + }, + "pascalCase": { + "unsafeName": "Cursor", + "safeName": "Cursor" + } + }, + "wireValue": "cursor" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": "The cursor for pagination" + }, + "propertyPath": null + }, + "next": { + "propertyPath": [], + "property": { + "name": { + "name": { + "originalName": "next", + "camelCase": { + "unsafeName": "next", + "safeName": "next" + }, + "snakeCase": { + "unsafeName": "next", + "safeName": "next" + }, + "screamingSnakeCase": { + "unsafeName": "NEXT", + "safeName": "NEXT" + }, + "pascalCase": { + "unsafeName": "Next", + "safeName": "Next" + } + }, + "wireValue": "next" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + }, + "results": { + "propertyPath": [], + "property": { + "name": { + "name": { + "originalName": "items", + "camelCase": { + "unsafeName": "items", + "safeName": "items" + }, + "snakeCase": { + "unsafeName": "items", + "safeName": "items" + }, + "screamingSnakeCase": { + "unsafeName": "ITEMS", + "safeName": "ITEMS" + }, + "pascalCase": { + "unsafeName": "Items", + "safeName": "Items" + } + }, + "wireValue": "items" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + } + }, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "List items with cursor pagination" + } + ], + "audiences": null + }, + "service_endpoints/params": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/params", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/params.getWithPath", + "name": { + "originalName": "getWithPath", + "camelCase": { + "unsafeName": "getWithPath", + "safeName": "getWithPath" + }, + "snakeCase": { + "unsafeName": "get_with_path", + "safeName": "get_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH", + "safeName": "GET_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithPath", + "safeName": "GetWithPath" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/params/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "8f1fbc60", + "url": "/params/path/param", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "param" + } + } + }, + "jsonExample": "param" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET with path param" + }, + { + "id": "endpoint_endpoints/params.getWithInlinePath", + "name": { + "originalName": "getWithInlinePath", + "camelCase": { + "unsafeName": "getWithInlinePath", + "safeName": "getWithInlinePath" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path", + "safeName": "get_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH", + "safeName": "GET_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePath", + "safeName": "GetWithInlinePath" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/params/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "getWithInlinePath", + "camelCase": { + "unsafeName": "getWithInlinePath", + "safeName": "getWithInlinePath" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path", + "safeName": "get_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH", + "safeName": "GET_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePath", + "safeName": "GetWithInlinePath" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": true, + "onlyPathParameters": true + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "8f1fbc60", + "url": "/params/path/param", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "param" + } + } + }, + "jsonExample": "param" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET with path param" + }, + { + "id": "endpoint_endpoints/params.getWithQuery", + "name": { + "originalName": "getWithQuery", + "camelCase": { + "unsafeName": "getWithQuery", + "safeName": "getWithQuery" + }, + "snakeCase": { + "unsafeName": "get_with_query", + "safeName": "get_with_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_QUERY", + "safeName": "GET_WITH_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithQuery", + "safeName": "GetWithQuery" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/params", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "GetWithQuery", + "camelCase": { + "unsafeName": "getWithQuery", + "safeName": "getWithQuery" + }, + "snakeCase": { + "unsafeName": "get_with_query", + "safeName": "get_with_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_QUERY", + "safeName": "GET_WITH_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithQuery", + "safeName": "GetWithQuery" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "18789af9", + "url": "/params", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "shape": { + "type": "single" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "query" + } + } + }, + "jsonExample": "query" + } + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "shape": { + "type": "single" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + } + ], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET with query param" + }, + { + "id": "endpoint_endpoints/params.getWithAllowMultipleQuery", + "name": { + "originalName": "getWithAllowMultipleQuery", + "camelCase": { + "unsafeName": "getWithAllowMultipleQuery", + "safeName": "getWithAllowMultipleQuery" + }, + "snakeCase": { + "unsafeName": "get_with_allow_multiple_query", + "safeName": "get_with_allow_multiple_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_ALLOW_MULTIPLE_QUERY", + "safeName": "GET_WITH_ALLOW_MULTIPLE_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithAllowMultipleQuery", + "safeName": "GetWithAllowMultipleQuery" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/params", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "allowMultiple": true, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "allowMultiple": true, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "GetWithMultipleQuery", + "camelCase": { + "unsafeName": "getWithMultipleQuery", + "safeName": "getWithMultipleQuery" + }, + "snakeCase": { + "unsafeName": "get_with_multiple_query", + "safeName": "get_with_multiple_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_MULTIPLE_QUERY", + "safeName": "GET_WITH_MULTIPLE_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithMultipleQuery", + "safeName": "GetWithMultipleQuery" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "78d2799f", + "url": "/params", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "shape": { + "type": "exploded" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "query" + } + } + }, + "jsonExample": "query" + } + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "shape": { + "type": "exploded" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + } + ], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET with multiple of same query param" + }, + { + "id": "endpoint_endpoints/params.getWithPathAndQuery", + "name": { + "originalName": "getWithPathAndQuery", + "camelCase": { + "unsafeName": "getWithPathAndQuery", + "safeName": "getWithPathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_path_and_query", + "safeName": "get_with_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH_AND_QUERY", + "safeName": "GET_WITH_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithPathAndQuery", + "safeName": "GetWithPathAndQuery" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/path-query/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/params/path-query/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "GetWithPathAndQuery", + "camelCase": { + "unsafeName": "getWithPathAndQuery", + "safeName": "getWithPathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_path_and_query", + "safeName": "get_with_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH_AND_QUERY", + "safeName": "GET_WITH_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithPathAndQuery", + "safeName": "GetWithPathAndQuery" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d0c0cbe4", + "url": "/params/path-query/param", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "param" + } + } + }, + "jsonExample": "param" + } + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "shape": { + "type": "single" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "query" + } + } + }, + "jsonExample": "query" + } + } + ], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET with path and query params" + }, + { + "id": "endpoint_endpoints/params.getWithInlinePathAndQuery", + "name": { + "originalName": "getWithInlinePathAndQuery", + "camelCase": { + "unsafeName": "getWithInlinePathAndQuery", + "safeName": "getWithInlinePathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path_and_query", + "safeName": "get_with_inline_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH_AND_QUERY", + "safeName": "GET_WITH_INLINE_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePathAndQuery", + "safeName": "GetWithInlinePathAndQuery" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/path-query/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/params/path-query/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "allowMultiple": false, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "availability": null, + "docs": null + } + ], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "getWithInlinePathAndQuery", + "camelCase": { + "unsafeName": "getWithInlinePathAndQuery", + "safeName": "getWithInlinePathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path_and_query", + "safeName": "get_with_inline_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH_AND_QUERY", + "safeName": "GET_WITH_INLINE_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePathAndQuery", + "safeName": "GetWithInlinePathAndQuery" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": true, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d0c0cbe4", + "url": "/params/path-query/param", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "param" + } + } + }, + "jsonExample": "param" + } + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "shape": { + "type": "single" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "query" + } + } + }, + "jsonExample": "query" + } + } + ], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "GET with path and query params" + }, + { + "id": "endpoint_endpoints/params.modifyWithPath", + "name": { + "originalName": "modifyWithPath", + "camelCase": { + "unsafeName": "modifyWithPath", + "safeName": "modifyWithPath" + }, + "snakeCase": { + "unsafeName": "modify_with_path", + "safeName": "modify_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_WITH_PATH", + "safeName": "MODIFY_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyWithPath", + "safeName": "ModifyWithPath" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "PUT", + "basePath": null, + "path": { + "head": "/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/params/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b71b8ba8", + "url": "/params/path/param", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "param" + } + } + }, + "jsonExample": "param" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "PUT to update with path param" + }, + { + "id": "endpoint_endpoints/params.modifyWithInlinePath", + "name": { + "originalName": "modifyWithInlinePath", + "camelCase": { + "unsafeName": "modifyWithInlinePath", + "safeName": "modifyWithInlinePath" + }, + "snakeCase": { + "unsafeName": "modify_with_inline_path", + "safeName": "modify_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_WITH_INLINE_PATH", + "safeName": "MODIFY_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyWithInlinePath", + "safeName": "ModifyWithInlinePath" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "PUT", + "basePath": null, + "path": { + "head": "/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/params/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ModifyResourceAtInlinedPath", + "camelCase": { + "unsafeName": "modifyResourceAtInlinedPath", + "safeName": "modifyResourceAtInlinedPath" + }, + "snakeCase": { + "unsafeName": "modify_resource_at_inlined_path", + "safeName": "modify_resource_at_inlined_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_RESOURCE_AT_INLINED_PATH", + "safeName": "MODIFY_RESOURCE_AT_INLINED_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyResourceAtInlinedPath", + "safeName": "ModifyResourceAtInlinedPath" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": true, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b71b8ba8", + "url": "/params/path/param", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "param" + } + } + }, + "jsonExample": "param" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "PUT to update with path param" + }, + { + "id": "endpoint_endpoints/params.uploadWithPath", + "name": { + "originalName": "uploadWithPath", + "camelCase": { + "unsafeName": "uploadWithPath", + "safeName": "uploadWithPath" + }, + "snakeCase": { + "unsafeName": "upload_with_path", + "safeName": "upload_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "UPLOAD_WITH_PATH", + "safeName": "UPLOAD_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "UploadWithPath", + "safeName": "UploadWithPath" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "fullPath": { + "head": "/params/path/", + "parts": [ + { + "pathParameter": "param", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "bytes", + "isOptional": false, + "docs": null, + "v2Examples": null, + "contentType": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "bytes", + "isOptional": false, + "docs": null, + "v2Examples": null, + "contentType": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithRequiredField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "b305eac1", + "name": null, + "url": "/params/path/upload-path", + "rootPathParameters": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "upload-path" + } + } + }, + "jsonExample": "upload-path" + } + } + ], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_types/object:ObjectWithRequiredField", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "uploaded" + } + } + }, + "jsonExample": "uploaded" + }, + "originalTypeDeclaration": { + "typeId": "type_types/object:ObjectWithRequiredField", + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "string": "uploaded" + } + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "POST bytes with path param returning object" + } + ], + "audiences": null + }, + "service_endpoints/primitive": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/primitive", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/primitive.getAndReturnString", + "name": { + "originalName": "getAndReturnString", + "camelCase": { + "unsafeName": "getAndReturnString", + "safeName": "getAndReturnString" + }, + "snakeCase": { + "unsafeName": "get_and_return_string", + "safeName": "get_and_return_string" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_STRING", + "safeName": "GET_AND_RETURN_STRING" + }, + "pascalCase": { + "unsafeName": "GetAndReturnString", + "safeName": "GetAndReturnString" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/string", + "parts": [] + }, + "fullPath": { + "head": "/primitive/string", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "63248c43", + "url": "/primitive/string", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnInt", + "name": { + "originalName": "getAndReturnInt", + "camelCase": { + "unsafeName": "getAndReturnInt", + "safeName": "getAndReturnInt" + }, + "snakeCase": { + "unsafeName": "get_and_return_int", + "safeName": "get_and_return_int" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_INT", + "safeName": "GET_AND_RETURN_INT" + }, + "pascalCase": { + "unsafeName": "GetAndReturnInt", + "safeName": "GetAndReturnInt" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/integer", + "parts": [] + }, + "fullPath": { + "head": "/primitive/integer", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "aacf1865", + "url": "/primitive/integer", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnLong", + "name": { + "originalName": "getAndReturnLong", + "camelCase": { + "unsafeName": "getAndReturnLong", + "safeName": "getAndReturnLong" + }, + "snakeCase": { + "unsafeName": "get_and_return_long", + "safeName": "get_and_return_long" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LONG", + "safeName": "GET_AND_RETURN_LONG" + }, + "pascalCase": { + "unsafeName": "GetAndReturnLong", + "safeName": "GetAndReturnLong" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/long", + "parts": [] + }, + "fullPath": { + "head": "/primitive/long", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "8944e061", + "url": "/primitive/long", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnDouble", + "name": { + "originalName": "getAndReturnDouble", + "camelCase": { + "unsafeName": "getAndReturnDouble", + "safeName": "getAndReturnDouble" + }, + "snakeCase": { + "unsafeName": "get_and_return_double", + "safeName": "get_and_return_double" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DOUBLE", + "safeName": "GET_AND_RETURN_DOUBLE" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDouble", + "safeName": "GetAndReturnDouble" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/double", + "parts": [] + }, + "fullPath": { + "head": "/primitive/double", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d4d0b11", + "url": "/primitive/double", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnBool", + "name": { + "originalName": "getAndReturnBool", + "camelCase": { + "unsafeName": "getAndReturnBool", + "safeName": "getAndReturnBool" + }, + "snakeCase": { + "unsafeName": "get_and_return_bool", + "safeName": "get_and_return_bool" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_BOOL", + "safeName": "GET_AND_RETURN_BOOL" + }, + "pascalCase": { + "unsafeName": "GetAndReturnBool", + "safeName": "GetAndReturnBool" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/boolean", + "parts": [] + }, + "fullPath": { + "head": "/primitive/boolean", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "f3e38285", + "url": "/primitive/boolean", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnDatetime", + "name": { + "originalName": "getAndReturnDatetime", + "camelCase": { + "unsafeName": "getAndReturnDatetime", + "safeName": "getAndReturnDatetime" + }, + "snakeCase": { + "unsafeName": "get_and_return_datetime", + "safeName": "get_and_return_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DATETIME", + "safeName": "GET_AND_RETURN_DATETIME" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDatetime", + "safeName": "GetAndReturnDatetime" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/datetime", + "parts": [] + }, + "fullPath": { + "head": "/primitive/datetime", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "fb513d11", + "url": "/primitive/datetime", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnDate", + "name": { + "originalName": "getAndReturnDate", + "camelCase": { + "unsafeName": "getAndReturnDate", + "safeName": "getAndReturnDate" + }, + "snakeCase": { + "unsafeName": "get_and_return_date", + "safeName": "get_and_return_date" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DATE", + "safeName": "GET_AND_RETURN_DATE" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDate", + "safeName": "GetAndReturnDate" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/date", + "parts": [] + }, + "fullPath": { + "head": "/primitive/date", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "eeb5648d", + "url": "/primitive/date", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnUUID", + "name": { + "originalName": "getAndReturnUUID", + "camelCase": { + "unsafeName": "getAndReturnUUID", + "safeName": "getAndReturnUUID" + }, + "snakeCase": { + "unsafeName": "get_and_return_uuid", + "safeName": "get_and_return_uuid" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_UUID", + "safeName": "GET_AND_RETURN_UUID" + }, + "pascalCase": { + "unsafeName": "GetAndReturnUUID", + "safeName": "GetAndReturnUUID" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/uuid", + "parts": [] + }, + "fullPath": { + "head": "/primitive/uuid", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "8a40715d", + "url": "/primitive/uuid", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/primitive.getAndReturnBase64", + "name": { + "originalName": "getAndReturnBase64", + "camelCase": { + "unsafeName": "getAndReturnBase64", + "safeName": "getAndReturnBase64" + }, + "snakeCase": { + "unsafeName": "get_and_return_base64", + "safeName": "get_and_return_base64" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_BASE64", + "safeName": "GET_AND_RETURN_BASE64" + }, + "pascalCase": { + "unsafeName": "GetAndReturnBase64", + "safeName": "GetAndReturnBase64" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/base64", + "parts": [] + }, + "fullPath": { + "head": "/primitive/base64", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "b885de1d", + "url": "/primitive/base64", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_endpoints/put": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/put.add", + "name": { + "originalName": "add", + "camelCase": { + "unsafeName": "add", + "safeName": "add" + }, + "snakeCase": { + "unsafeName": "add", + "safeName": "add" + }, + "screamingSnakeCase": { + "unsafeName": "ADD", + "safeName": "ADD" + }, + "pascalCase": { + "unsafeName": "Add", + "safeName": "Add" + } + }, + "displayName": "Put", + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "PUT", + "basePath": null, + "path": { + "head": "/", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "fullPath": { + "head": "", + "parts": [ + { + "pathParameter": "id", + "tail": "" + } + ] + }, + "pathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "allPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "location": "ENDPOINT", + "variable": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "explode": null, + "docs": null + } + ], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "PutRequest", + "camelCase": { + "unsafeName": "putRequest", + "safeName": "putRequest" + }, + "snakeCase": { + "unsafeName": "put_request", + "safeName": "put_request" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_REQUEST", + "safeName": "PUT_REQUEST" + }, + "pascalCase": { + "unsafeName": "PutRequest", + "safeName": "PutRequest" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": true, + "onlyPathParameters": true + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "PutResponse", + "camelCase": { + "unsafeName": "putResponse", + "safeName": "putResponse" + }, + "snakeCase": { + "unsafeName": "put_response", + "safeName": "put_response" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_RESPONSE", + "safeName": "PUT_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PutResponse", + "safeName": "PutResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:PutResponse", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": 200, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "621d8b0", + "url": "/id", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [ + { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "id" + } + } + }, + "jsonExample": "id" + } + } + ], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "errors", + "camelCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "snakeCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "screamingSnakeCase": { + "unsafeName": "ERRORS", + "safeName": "ERRORS" + }, + "pascalCase": { + "unsafeName": "Errors", + "safeName": "Errors" + } + }, + "wireValue": "errors" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "PutResponse", + "camelCase": { + "unsafeName": "putResponse", + "safeName": "putResponse" + }, + "snakeCase": { + "unsafeName": "put_response", + "safeName": "put_response" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_RESPONSE", + "safeName": "PUT_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PutResponse", + "safeName": "PutResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:PutResponse" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "category", + "camelCase": { + "unsafeName": "category", + "safeName": "category" + }, + "snakeCase": { + "unsafeName": "category", + "safeName": "category" + }, + "screamingSnakeCase": { + "unsafeName": "CATEGORY", + "safeName": "CATEGORY" + }, + "pascalCase": { + "unsafeName": "Category", + "safeName": "Category" + } + }, + "wireValue": "category" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "enum", + "value": { + "name": { + "originalName": "API_ERROR", + "camelCase": { + "unsafeName": "apiError", + "safeName": "apiError" + }, + "snakeCase": { + "unsafeName": "api_error", + "safeName": "api_error" + }, + "screamingSnakeCase": { + "unsafeName": "API_ERROR", + "safeName": "API_ERROR" + }, + "pascalCase": { + "unsafeName": "APIError", + "safeName": "APIError" + } + }, + "wireValue": "API_ERROR" + } + }, + "typeName": { + "name": { + "originalName": "ErrorCategory", + "camelCase": { + "unsafeName": "errorCategory", + "safeName": "errorCategory" + }, + "snakeCase": { + "unsafeName": "error_category", + "safeName": "error_category" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CATEGORY", + "safeName": "ERROR_CATEGORY" + }, + "pascalCase": { + "unsafeName": "ErrorCategory", + "safeName": "ErrorCategory" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCategory" + } + }, + "jsonExample": "API_ERROR" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "code", + "camelCase": { + "unsafeName": "code", + "safeName": "code" + }, + "snakeCase": { + "unsafeName": "code", + "safeName": "code" + }, + "screamingSnakeCase": { + "unsafeName": "CODE", + "safeName": "CODE" + }, + "pascalCase": { + "unsafeName": "Code", + "safeName": "Code" + } + }, + "wireValue": "code" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "enum", + "value": { + "name": { + "originalName": "INTERNAL_SERVER_ERROR", + "camelCase": { + "unsafeName": "internalServerError", + "safeName": "internalServerError" + }, + "snakeCase": { + "unsafeName": "internal_server_error", + "safeName": "internal_server_error" + }, + "screamingSnakeCase": { + "unsafeName": "INTERNAL_SERVER_ERROR", + "safeName": "INTERNAL_SERVER_ERROR" + }, + "pascalCase": { + "unsafeName": "InternalServerError", + "safeName": "InternalServerError" + } + }, + "wireValue": "INTERNAL_SERVER_ERROR" + } + }, + "typeName": { + "name": { + "originalName": "ErrorCode", + "camelCase": { + "unsafeName": "errorCode", + "safeName": "errorCode" + }, + "snakeCase": { + "unsafeName": "error_code", + "safeName": "error_code" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CODE", + "safeName": "ERROR_CODE" + }, + "pascalCase": { + "unsafeName": "ErrorCode", + "safeName": "ErrorCode" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCode" + } + }, + "jsonExample": "INTERNAL_SERVER_ERROR" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "detail", + "camelCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "snakeCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "screamingSnakeCase": { + "unsafeName": "DETAIL", + "safeName": "DETAIL" + }, + "pascalCase": { + "unsafeName": "Detail", + "safeName": "Detail" + } + }, + "wireValue": "detail" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "detail" + } + } + }, + "jsonExample": "detail" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "detail" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "field", + "camelCase": { + "unsafeName": "field", + "safeName": "field" + }, + "snakeCase": { + "unsafeName": "field", + "safeName": "field" + }, + "screamingSnakeCase": { + "unsafeName": "FIELD", + "safeName": "FIELD" + }, + "pascalCase": { + "unsafeName": "Field", + "safeName": "Field" + } + }, + "wireValue": "field" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "field" + } + } + }, + "jsonExample": "field" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "field" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + } + }, + "jsonExample": { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + } + }, + { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "category", + "camelCase": { + "unsafeName": "category", + "safeName": "category" + }, + "snakeCase": { + "unsafeName": "category", + "safeName": "category" + }, + "screamingSnakeCase": { + "unsafeName": "CATEGORY", + "safeName": "CATEGORY" + }, + "pascalCase": { + "unsafeName": "Category", + "safeName": "Category" + } + }, + "wireValue": "category" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "enum", + "value": { + "name": { + "originalName": "API_ERROR", + "camelCase": { + "unsafeName": "apiError", + "safeName": "apiError" + }, + "snakeCase": { + "unsafeName": "api_error", + "safeName": "api_error" + }, + "screamingSnakeCase": { + "unsafeName": "API_ERROR", + "safeName": "API_ERROR" + }, + "pascalCase": { + "unsafeName": "APIError", + "safeName": "APIError" + } + }, + "wireValue": "API_ERROR" + } + }, + "typeName": { + "name": { + "originalName": "ErrorCategory", + "camelCase": { + "unsafeName": "errorCategory", + "safeName": "errorCategory" + }, + "snakeCase": { + "unsafeName": "error_category", + "safeName": "error_category" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CATEGORY", + "safeName": "ERROR_CATEGORY" + }, + "pascalCase": { + "unsafeName": "ErrorCategory", + "safeName": "ErrorCategory" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCategory" + } + }, + "jsonExample": "API_ERROR" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "code", + "camelCase": { + "unsafeName": "code", + "safeName": "code" + }, + "snakeCase": { + "unsafeName": "code", + "safeName": "code" + }, + "screamingSnakeCase": { + "unsafeName": "CODE", + "safeName": "CODE" + }, + "pascalCase": { + "unsafeName": "Code", + "safeName": "Code" + } + }, + "wireValue": "code" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "enum", + "value": { + "name": { + "originalName": "INTERNAL_SERVER_ERROR", + "camelCase": { + "unsafeName": "internalServerError", + "safeName": "internalServerError" + }, + "snakeCase": { + "unsafeName": "internal_server_error", + "safeName": "internal_server_error" + }, + "screamingSnakeCase": { + "unsafeName": "INTERNAL_SERVER_ERROR", + "safeName": "INTERNAL_SERVER_ERROR" + }, + "pascalCase": { + "unsafeName": "InternalServerError", + "safeName": "InternalServerError" + } + }, + "wireValue": "INTERNAL_SERVER_ERROR" + } + }, + "typeName": { + "name": { + "originalName": "ErrorCode", + "camelCase": { + "unsafeName": "errorCode", + "safeName": "errorCode" + }, + "snakeCase": { + "unsafeName": "error_code", + "safeName": "error_code" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CODE", + "safeName": "ERROR_CODE" + }, + "pascalCase": { + "unsafeName": "ErrorCode", + "safeName": "ErrorCode" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:ErrorCode" + } + }, + "jsonExample": "INTERNAL_SERVER_ERROR" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "detail", + "camelCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "snakeCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "screamingSnakeCase": { + "unsafeName": "DETAIL", + "safeName": "DETAIL" + }, + "pascalCase": { + "unsafeName": "Detail", + "safeName": "Detail" + } + }, + "wireValue": "detail" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "detail" + } + } + }, + "jsonExample": "detail" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "detail" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "field", + "camelCase": { + "unsafeName": "field", + "safeName": "field" + }, + "snakeCase": { + "unsafeName": "field", + "safeName": "field" + }, + "screamingSnakeCase": { + "unsafeName": "FIELD", + "safeName": "FIELD" + }, + "pascalCase": { + "unsafeName": "Field", + "safeName": "Field" + } + }, + "wireValue": "field" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "field" + } + } + }, + "jsonExample": "field" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "field" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error" + } + }, + "jsonExample": { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + } + } + ], + "itemType": { + "_type": "named", + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error", + "default": null, + "inline": null + } + } + }, + "jsonExample": [ + { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + }, + { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + } + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "named", + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:Error", + "default": null, + "inline": null + } + } + } + } + }, + "jsonExample": [ + { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + }, + { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + } + ] + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "PutResponse", + "camelCase": { + "unsafeName": "putResponse", + "safeName": "putResponse" + }, + "snakeCase": { + "unsafeName": "put_response", + "safeName": "put_response" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_RESPONSE", + "safeName": "PUT_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PutResponse", + "safeName": "PutResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "displayName": null, + "typeId": "type_endpoints/put:PutResponse" + } + }, + "jsonExample": { + "errors": [ + { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + }, + { + "category": "API_ERROR", + "code": "INTERNAL_SERVER_ERROR", + "detail": "detail", + "field": "field" + } + ] + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_endpoints/union": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/union", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/union.getAndReturnUnion", + "name": { + "originalName": "getAndReturnUnion", + "camelCase": { + "unsafeName": "getAndReturnUnion", + "safeName": "getAndReturnUnion" + }, + "snakeCase": { + "unsafeName": "get_and_return_union", + "safeName": "get_and_return_union" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_UNION", + "safeName": "GET_AND_RETURN_UNION" + }, + "pascalCase": { + "unsafeName": "GetAndReturnUnion", + "safeName": "GetAndReturnUnion" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/union", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Animal", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Animal", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Animal", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "8ec2c7d1", + "url": "/union", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "union", + "discriminant": { + "name": { + "originalName": "animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "wireValue": "animal" + }, + "singleUnionType": { + "wireDiscriminantValue": { + "name": { + "originalName": "dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "wireValue": "dog" + }, + "shape": { + "type": "samePropertiesAsObject", + "typeId": "type_types/union:Dog", + "object": { + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Dog" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "name" + } + } + }, + "jsonExample": "name" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "likesToWoof", + "camelCase": { + "unsafeName": "likesToWoof", + "safeName": "likesToWoof" + }, + "snakeCase": { + "unsafeName": "likes_to_woof", + "safeName": "likes_to_woof" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_WOOF", + "safeName": "LIKES_TO_WOOF" + }, + "pascalCase": { + "unsafeName": "LikesToWoof", + "safeName": "LikesToWoof" + } + }, + "wireValue": "likesToWoof" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Dog" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + } + }, + "baseProperties": [], + "extendProperties": [] + }, + "typeName": { + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Animal" + } + }, + "jsonExample": { + "animal": "dog", + "name": "name", + "likesToWoof": true + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "union", + "discriminant": { + "name": { + "originalName": "animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "wireValue": "animal" + }, + "singleUnionType": { + "wireDiscriminantValue": { + "name": { + "originalName": "dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "wireValue": "dog" + }, + "shape": { + "type": "samePropertiesAsObject", + "typeId": "type_types/union:Dog", + "object": { + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Dog" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "name" + } + } + }, + "jsonExample": "name" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "likesToWoof", + "camelCase": { + "unsafeName": "likesToWoof", + "safeName": "likesToWoof" + }, + "snakeCase": { + "unsafeName": "likes_to_woof", + "safeName": "likes_to_woof" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_WOOF", + "safeName": "LIKES_TO_WOOF" + }, + "pascalCase": { + "unsafeName": "LikesToWoof", + "safeName": "LikesToWoof" + } + }, + "wireValue": "likesToWoof" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Dog" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + } + }, + "baseProperties": [], + "extendProperties": [] + }, + "typeName": { + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "displayName": null, + "typeId": "type_types/union:Animal" + } + }, + "jsonExample": { + "animal": "dog", + "name": "name", + "likesToWoof": true + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_endpoints/urls": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/urls", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_endpoints/urls.withMixedCase", + "name": { + "originalName": "withMixedCase", + "camelCase": { + "unsafeName": "withMixedCase", + "safeName": "withMixedCase" + }, + "snakeCase": { + "unsafeName": "with_mixed_case", + "safeName": "with_mixed_case" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_MIXED_CASE", + "safeName": "WITH_MIXED_CASE" + }, + "pascalCase": { + "unsafeName": "WithMixedCase", + "safeName": "WithMixedCase" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/MixedCase", + "parts": [] + }, + "fullPath": { + "head": "/urls/MixedCase", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d9186361", + "url": "/urls/MixedCase", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/urls.noEndingSlash", + "name": { + "originalName": "noEndingSlash", + "camelCase": { + "unsafeName": "noEndingSlash", + "safeName": "noEndingSlash" + }, + "snakeCase": { + "unsafeName": "no_ending_slash", + "safeName": "no_ending_slash" + }, + "screamingSnakeCase": { + "unsafeName": "NO_ENDING_SLASH", + "safeName": "NO_ENDING_SLASH" + }, + "pascalCase": { + "unsafeName": "NoEndingSlash", + "safeName": "NoEndingSlash" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/no-ending-slash", + "parts": [] + }, + "fullPath": { + "head": "/urls/no-ending-slash", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d9186361", + "url": "/urls/no-ending-slash", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/urls.withEndingSlash", + "name": { + "originalName": "withEndingSlash", + "camelCase": { + "unsafeName": "withEndingSlash", + "safeName": "withEndingSlash" + }, + "snakeCase": { + "unsafeName": "with_ending_slash", + "safeName": "with_ending_slash" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_ENDING_SLASH", + "safeName": "WITH_ENDING_SLASH" + }, + "pascalCase": { + "unsafeName": "WithEndingSlash", + "safeName": "WithEndingSlash" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/with-ending-slash/", + "parts": [] + }, + "fullPath": { + "head": "/urls/with-ending-slash/", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d9186361", + "url": "/urls/with-ending-slash/", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_endpoints/urls.withUnderscores", + "name": { + "originalName": "withUnderscores", + "camelCase": { + "unsafeName": "withUnderscores", + "safeName": "withUnderscores" + }, + "snakeCase": { + "unsafeName": "with_underscores", + "safeName": "with_underscores" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_UNDERSCORES", + "safeName": "WITH_UNDERSCORES" + }, + "pascalCase": { + "unsafeName": "WithUnderscores", + "safeName": "WithUnderscores" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "/with_underscores", + "parts": [] + }, + "fullPath": { + "head": "/urls/with_underscores", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d9186361", + "url": "/urls/with_underscores", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_inlined-requests": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + ], + "packagePath": [], + "file": { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/req-bodies", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_inlined-requests.postWithObjectBodyandResponse", + "name": { + "originalName": "postWithObjectBodyandResponse", + "camelCase": { + "unsafeName": "postWithObjectBodyandResponse", + "safeName": "postWithObjectBodyandResponse" + }, + "snakeCase": { + "unsafeName": "post_with_object_bodyand_response", + "safeName": "post_with_object_bodyand_response" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_OBJECT_BODYAND_RESPONSE", + "safeName": "POST_WITH_OBJECT_BODYAND_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PostWithObjectBodyandResponse", + "safeName": "PostWithObjectBodyandResponse" + } + }, + "displayName": null, + "auth": false, + "security": null, + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/object", + "parts": [] + }, + "fullPath": { + "head": "/req-bodies/object", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "inlinedRequestBody", + "name": { + "originalName": "PostWithObjectBody", + "camelCase": { + "unsafeName": "postWithObjectBody", + "safeName": "postWithObjectBody" + }, + "snakeCase": { + "unsafeName": "post_with_object_body", + "safeName": "post_with_object_body" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_OBJECT_BODY", + "safeName": "POST_WITH_OBJECT_BODY" + }, + "pascalCase": { + "unsafeName": "PostWithObjectBody", + "safeName": "PostWithObjectBody" + } + }, + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "valueType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "propertyAccess": null, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [], + "docs": null, + "v2Examples": null, + "contentType": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "PostWithObjectBody", + "camelCase": { + "unsafeName": "postWithObjectBody", + "safeName": "postWithObjectBody" + }, + "snakeCase": { + "unsafeName": "post_with_object_body", + "safeName": "post_with_object_body" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_OBJECT_BODY", + "safeName": "POST_WITH_OBJECT_BODY" + }, + "pascalCase": { + "unsafeName": "PostWithObjectBody", + "safeName": "PostWithObjectBody" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [ + { + "error": { + "name": { + "originalName": "BadRequestBody", + "camelCase": { + "unsafeName": "badRequestBody", + "safeName": "badRequestBody" + }, + "snakeCase": { + "unsafeName": "bad_request_body", + "safeName": "bad_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST_BODY", + "safeName": "BAD_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "BadRequestBody", + "safeName": "BadRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "errorId": "error_general-errors:BadRequestBody" + }, + "docs": null + } + ], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "16365eaf", + "url": "/req-bodies/object", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + } + } + ], + "extraProperties": null, + "jsonExample": { + "string": "string", + "integer": 1, + "NestedObject": {} + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + }, + { + "example": { + "id": "eec063d1", + "url": "/req-bodies/object", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "inlinedRequestBody", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + } + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "originalTypeDeclaration": null, + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": null, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + } + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": {} + } + } + ], + "extraProperties": null, + "jsonExample": { + "string": "string", + "integer": 1, + "NestedObject": {} + } + }, + "response": { + "type": "error", + "body": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "message", + "camelCase": { + "unsafeName": "message", + "safeName": "message" + }, + "snakeCase": { + "unsafeName": "message", + "safeName": "message" + }, + "screamingSnakeCase": { + "unsafeName": "MESSAGE", + "safeName": "MESSAGE" + }, + "pascalCase": { + "unsafeName": "Message", + "safeName": "Message" + } + }, + "wireValue": "message" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "displayName": null, + "typeId": "type_general-errors:BadObjectRequestInfo" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "message" + } + } + }, + "jsonExample": "message" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "displayName": null, + "typeId": "type_general-errors:BadObjectRequestInfo" + } + }, + "jsonExample": { + "message": "message" + } + }, + "error": { + "name": { + "originalName": "BadRequestBody", + "camelCase": { + "unsafeName": "badRequestBody", + "safeName": "badRequestBody" + }, + "snakeCase": { + "unsafeName": "bad_request_body", + "safeName": "bad_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST_BODY", + "safeName": "BAD_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "BadRequestBody", + "safeName": "BadRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "errorId": "error_general-errors:BadRequestBody" + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "POST with custom object in request body, response is an object" + } + ], + "audiences": null + }, + "service_no-auth": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/no-auth", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_no-auth.postWithNoAuth", + "name": { + "originalName": "postWithNoAuth", + "camelCase": { + "unsafeName": "postWithNoAuth", + "safeName": "postWithNoAuth" + }, + "snakeCase": { + "unsafeName": "post_with_no_auth", + "safeName": "post_with_no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_NO_AUTH", + "safeName": "POST_WITH_NO_AUTH" + }, + "pascalCase": { + "unsafeName": "PostWithNoAuth", + "safeName": "PostWithNoAuth" + } + }, + "displayName": null, + "auth": false, + "security": null, + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/no-auth", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "unknown" + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "unknown" + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [ + { + "error": { + "name": { + "originalName": "BadRequestBody", + "camelCase": { + "unsafeName": "badRequestBody", + "safeName": "badRequestBody" + }, + "snakeCase": { + "unsafeName": "bad_request_body", + "safeName": "bad_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST_BODY", + "safeName": "BAD_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "BadRequestBody", + "safeName": "BadRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "errorId": "error_general-errors:BadRequestBody" + }, + "docs": null + } + ], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "69d1a307", + "url": "/no-auth", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "unknown", + "unknown": { + "key": "value" + } + }, + "jsonExample": { + "key": "value" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + } + } + }, + "docs": null + } + }, + { + "example": { + "id": "26ce45bc", + "url": "/no-auth", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "unknown", + "unknown": { + "key": "value" + } + }, + "jsonExample": { + "key": "value" + } + }, + "response": { + "type": "error", + "body": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "message", + "camelCase": { + "unsafeName": "message", + "safeName": "message" + }, + "snakeCase": { + "unsafeName": "message", + "safeName": "message" + }, + "screamingSnakeCase": { + "unsafeName": "MESSAGE", + "safeName": "MESSAGE" + }, + "pascalCase": { + "unsafeName": "Message", + "safeName": "Message" + } + }, + "wireValue": "message" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "displayName": null, + "typeId": "type_general-errors:BadObjectRequestInfo" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "message" + } + } + }, + "jsonExample": "message" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "displayName": null, + "typeId": "type_general-errors:BadObjectRequestInfo" + } + }, + "jsonExample": { + "message": "message" + } + }, + "error": { + "name": { + "originalName": "BadRequestBody", + "camelCase": { + "unsafeName": "badRequestBody", + "safeName": "badRequestBody" + }, + "snakeCase": { + "unsafeName": "bad_request_body", + "safeName": "bad_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST_BODY", + "safeName": "BAD_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "BadRequestBody", + "safeName": "BadRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "errorId": "error_general-errors:BadRequestBody" + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": "POST request with no auth" + } + ], + "audiences": null + }, + "service_no-req-body": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/no-req-body", + "parts": [] + }, + "headers": [], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_no-req-body.getWithNoRequestBody", + "name": { + "originalName": "getWithNoRequestBody", + "camelCase": { + "unsafeName": "getWithNoRequestBody", + "safeName": "getWithNoRequestBody" + }, + "snakeCase": { + "unsafeName": "get_with_no_request_body", + "safeName": "get_with_no_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_NO_REQUEST_BODY", + "safeName": "GET_WITH_NO_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "GetWithNoRequestBody", + "safeName": "GetWithNoRequestBody" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "GET", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/no-req-body", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField", + "default": null, + "inline": null + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "ccc30f9b", + "url": "/no-req-body", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "string" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "long", + "long": 1000000 + } + }, + "jsonExample": 1000000 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "LONG", + "v2": { + "type": "long", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1000000 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "double", + "double": 1.1 + } + }, + "jsonExample": 1.1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DOUBLE", + "v2": { + "type": "double", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1.1 + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "boolean", + "boolean": true + } + }, + "jsonExample": true + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BOOLEAN", + "v2": { + "type": "boolean", + "default": null + } + } + } + } + }, + "jsonExample": true + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "datetime", + "datetime": "2024-01-15T09:30:00.000Z", + "raw": "2024-01-15T09:30:00Z" + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE_TIME", + "v2": null + } + } + } + }, + "jsonExample": "2024-01-15T09:30:00Z" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "date", + "date": "2023-01-15" + } + }, + "jsonExample": "2023-01-15" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "DATE", + "v2": null + } + } + } + }, + "jsonExample": "2023-01-15" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "uuid", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "UUID", + "v2": null + } + } + } + }, + "jsonExample": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "base64", + "base64": "SGVsbG8gd29ybGQh" + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BASE_64", + "v2": null + } + } + } + }, + "jsonExample": "SGVsbG8gd29ybGQh" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "list", + "list": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + }, + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "list" + } + } + }, + "jsonExample": "list" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "list", + "list": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "list", + "list" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "set", + "set": [ + { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "set" + } + } + }, + "jsonExample": "set" + } + ], + "itemType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "valueType": { + "_type": "container", + "container": { + "_type": "set", + "set": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": [ + "set" + ] + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "container", + "container": { + "type": "map", + "map": [ + { + "key": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "map" + } + } + }, + "jsonExample": "map" + } + } + ], + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "valueType": { + "_type": "container", + "container": { + "_type": "map", + "keyType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + } + } + }, + "jsonExample": { + "1": "map" + } + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "bigInteger", + "bigInteger": "1000000" + } + }, + "jsonExample": "1000000" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "BIG_INTEGER", + "v2": { + "type": "bigInteger", + "default": null + } + } + } + } + }, + "jsonExample": "1000000" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "displayName": null, + "typeId": "type_types/object:ObjectWithOptionalField" + } + }, + "jsonExample": { + "string": "string", + "integer": 1, + "long": 1000000, + "double": 1.1, + "bool": true, + "datetime": "2024-01-15T09:30:00Z", + "date": "2023-01-15", + "uuid": "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + "base64": "SGVsbG8gd29ybGQh", + "list": [ + "list", + "list" + ], + "set": [ + "set" + ], + "map": { + "1": "map" + }, + "bigint": "1000000" + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + }, + { + "id": "endpoint_no-req-body.postWithNoRequestBody", + "name": { + "originalName": "postWithNoRequestBody", + "camelCase": { + "unsafeName": "postWithNoRequestBody", + "safeName": "postWithNoRequestBody" + }, + "snakeCase": { + "unsafeName": "post_with_no_request_body", + "safeName": "post_with_no_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_NO_REQUEST_BODY", + "safeName": "POST_WITH_NO_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "PostWithNoRequestBody", + "safeName": "PostWithNoRequestBody" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "", + "parts": [] + }, + "fullPath": { + "head": "/no-req-body", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": null, + "v2RequestBodies": null, + "sdkRequest": null, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "v2Examples": null + } + }, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "d9186361", + "url": "/no-req-body", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": null, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + }, + "service_req-with-headers": { + "availability": null, + "name": { + "fernFilepath": { + "allParts": [ + { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + ], + "packagePath": [], + "file": { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + } + }, + "displayName": null, + "basePath": { + "head": "/test-headers", + "parts": [] + }, + "headers": [ + { + "name": { + "name": { + "originalName": "X-TEST-SERVICE-HEADER", + "camelCase": { + "unsafeName": "xTestServiceHeader", + "safeName": "xTestServiceHeader" + }, + "snakeCase": { + "unsafeName": "x_test_service_header", + "safeName": "x_test_service_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_SERVICE_HEADER", + "safeName": "X_TEST_SERVICE_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestServiceHeader", + "safeName": "XTestServiceHeader" + } + }, + "wireValue": "X-TEST-SERVICE-HEADER" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "env": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "pathParameters": [], + "encoding": { + "json": {}, + "proto": null + }, + "transport": { + "type": "http" + }, + "endpoints": [ + { + "id": "endpoint_req-with-headers.getWithCustomHeader", + "name": { + "originalName": "getWithCustomHeader", + "camelCase": { + "unsafeName": "getWithCustomHeader", + "safeName": "getWithCustomHeader" + }, + "snakeCase": { + "unsafeName": "get_with_custom_header", + "safeName": "get_with_custom_header" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_CUSTOM_HEADER", + "safeName": "GET_WITH_CUSTOM_HEADER" + }, + "pascalCase": { + "unsafeName": "GetWithCustomHeader", + "safeName": "GetWithCustomHeader" + } + }, + "displayName": null, + "auth": true, + "security": [ + { + "bearer": [] + } + ], + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/custom-header", + "parts": [] + }, + "fullPath": { + "head": "/test-headers/custom-header", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [ + { + "name": { + "name": { + "originalName": "X-TEST-ENDPOINT-HEADER", + "camelCase": { + "unsafeName": "xTestEndpointHeader", + "safeName": "xTestEndpointHeader" + }, + "snakeCase": { + "unsafeName": "x_test_endpoint_header", + "safeName": "x_test_endpoint_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_ENDPOINT_HEADER", + "safeName": "X_TEST_ENDPOINT_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestEndpointHeader", + "safeName": "XTestEndpointHeader" + } + }, + "wireValue": "X-TEST-ENDPOINT-HEADER" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "env": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "docs": null, + "contentType": null, + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "wrapper", + "wrapperName": { + "originalName": "ReqWithHeaders", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + }, + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "includePathParameters": false, + "onlyPathParameters": false + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": null, + "status-code": null, + "isWildcardStatusCode": null, + "docs": null + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [], + "autogeneratedExamples": [ + { + "example": { + "id": "2da43e2c", + "url": "/test-headers/custom-header", + "name": null, + "endpointHeaders": [ + { + "name": { + "name": { + "originalName": "X-TEST-ENDPOINT-HEADER", + "camelCase": { + "unsafeName": "xTestEndpointHeader", + "safeName": "xTestEndpointHeader" + }, + "snakeCase": { + "unsafeName": "x_test_endpoint_header", + "safeName": "x_test_endpoint_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_ENDPOINT_HEADER", + "safeName": "X_TEST_ENDPOINT_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestEndpointHeader", + "safeName": "XTestEndpointHeader" + } + }, + "wireValue": "X-TEST-ENDPOINT-HEADER" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "X-TEST-ENDPOINT-HEADER" + } + } + }, + "jsonExample": "X-TEST-ENDPOINT-HEADER" + } + } + ], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [ + { + "name": { + "name": { + "originalName": "X-TEST-SERVICE-HEADER", + "camelCase": { + "unsafeName": "xTestServiceHeader", + "safeName": "xTestServiceHeader" + }, + "snakeCase": { + "unsafeName": "x_test_service_header", + "safeName": "x_test_service_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_SERVICE_HEADER", + "safeName": "X_TEST_SERVICE_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestServiceHeader", + "safeName": "XTestServiceHeader" + } + }, + "wireValue": "X-TEST-SERVICE-HEADER" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "X-TEST-SERVICE-HEADER" + } + } + }, + "jsonExample": "X-TEST-SERVICE-HEADER" + } + } + ], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "string" + } + } + }, + "jsonExample": "string" + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": null + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + } + }, + "constants": { + "errorInstanceIdKey": { + "name": { + "originalName": "errorInstanceId", + "camelCase": { + "unsafeName": "errorInstanceID", + "safeName": "errorInstanceID" + }, + "snakeCase": { + "unsafeName": "error_instance_id", + "safeName": "error_instance_id" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_INSTANCE_ID", + "safeName": "ERROR_INSTANCE_ID" + }, + "pascalCase": { + "unsafeName": "ErrorInstanceID", + "safeName": "ErrorInstanceID" + } + }, + "wireValue": "errorInstanceId" + } + }, + "environments": null, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": { + "service_endpoints/pagination": [ + "type_endpoints/pagination:PaginatedResponse" + ], + "service_endpoints/put": [ + "type_endpoints/put:Error", + "type_endpoints/put:ErrorCategory", + "type_endpoints/put:ErrorCode", + "type_endpoints/put:PutResponse" + ], + "service_endpoints/enum": [ + "type_types/enum:WeatherReport" + ], + "service_endpoints/object": [ + "type_types/object:ObjectWithMapOfMap", + "type_types/object:NestedObjectWithOptionalField", + "type_types/object:NestedObjectWithRequiredField", + "type_types/object:ObjectWithDatetimeLikeString", + "type_types/object:ObjectWithUnknownField" + ], + "service_endpoints/union": [ + "type_types/union:Animal", + "type_types/union:Dog", + "type_types/union:Cat" + ], + "service_endpoints/container": [ + "type_types/union:MixedType" + ] + }, + "sharedTypes": [ + "type_general-errors:BadObjectRequestInfo", + "type_types/docs:ObjectWithDocs", + "type_types/object:ObjectWithOptionalField", + "type_types/object:ObjectWithRequiredField", + "type_types/object:DoubleOptional", + "type_types/object:OptionalAlias" + ] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": { + "type_endpoints/pagination:PaginatedResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "PaginatedResponse", + "camelCase": { + "unsafeName": "paginatedResponse", + "safeName": "paginatedResponse" + }, + "snakeCase": { + "unsafeName": "paginated_response", + "safeName": "paginated_response" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATED_RESPONSE", + "safeName": "PAGINATED_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PaginatedResponse", + "safeName": "PaginatedResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "items", + "camelCase": { + "unsafeName": "items", + "safeName": "items" + }, + "snakeCase": { + "unsafeName": "items", + "safeName": "items" + }, + "screamingSnakeCase": { + "unsafeName": "ITEMS", + "safeName": "ITEMS" + }, + "pascalCase": { + "unsafeName": "Items", + "safeName": "Items" + } + }, + "wireValue": "items" + }, + "typeReference": { + "type": "list", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "next", + "camelCase": { + "unsafeName": "next", + "safeName": "next" + }, + "snakeCase": { + "unsafeName": "next", + "safeName": "next" + }, + "screamingSnakeCase": { + "unsafeName": "NEXT", + "safeName": "NEXT" + }, + "pascalCase": { + "unsafeName": "Next", + "safeName": "Next" + } + }, + "wireValue": "next" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_endpoints/put:Error": { + "type": "object", + "declaration": { + "name": { + "originalName": "Error", + "camelCase": { + "unsafeName": "error", + "safeName": "error" + }, + "snakeCase": { + "unsafeName": "error", + "safeName": "error" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR", + "safeName": "ERROR" + }, + "pascalCase": { + "unsafeName": "Error", + "safeName": "Error" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "category", + "camelCase": { + "unsafeName": "category", + "safeName": "category" + }, + "snakeCase": { + "unsafeName": "category", + "safeName": "category" + }, + "screamingSnakeCase": { + "unsafeName": "CATEGORY", + "safeName": "CATEGORY" + }, + "pascalCase": { + "unsafeName": "Category", + "safeName": "Category" + } + }, + "wireValue": "category" + }, + "typeReference": { + "type": "named", + "value": "type_endpoints/put:ErrorCategory" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "code", + "camelCase": { + "unsafeName": "code", + "safeName": "code" + }, + "snakeCase": { + "unsafeName": "code", + "safeName": "code" + }, + "screamingSnakeCase": { + "unsafeName": "CODE", + "safeName": "CODE" + }, + "pascalCase": { + "unsafeName": "Code", + "safeName": "Code" + } + }, + "wireValue": "code" + }, + "typeReference": { + "type": "named", + "value": "type_endpoints/put:ErrorCode" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "detail", + "camelCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "snakeCase": { + "unsafeName": "detail", + "safeName": "detail" + }, + "screamingSnakeCase": { + "unsafeName": "DETAIL", + "safeName": "DETAIL" + }, + "pascalCase": { + "unsafeName": "Detail", + "safeName": "Detail" + } + }, + "wireValue": "detail" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "field", + "camelCase": { + "unsafeName": "field", + "safeName": "field" + }, + "snakeCase": { + "unsafeName": "field", + "safeName": "field" + }, + "screamingSnakeCase": { + "unsafeName": "FIELD", + "safeName": "FIELD" + }, + "pascalCase": { + "unsafeName": "Field", + "safeName": "Field" + } + }, + "wireValue": "field" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_endpoints/put:ErrorCategory": { + "type": "enum", + "declaration": { + "name": { + "originalName": "ErrorCategory", + "camelCase": { + "unsafeName": "errorCategory", + "safeName": "errorCategory" + }, + "snakeCase": { + "unsafeName": "error_category", + "safeName": "error_category" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CATEGORY", + "safeName": "ERROR_CATEGORY" + }, + "pascalCase": { + "unsafeName": "ErrorCategory", + "safeName": "ErrorCategory" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "values": [ + { + "name": { + "originalName": "API_ERROR", + "camelCase": { + "unsafeName": "apiError", + "safeName": "apiError" + }, + "snakeCase": { + "unsafeName": "api_error", + "safeName": "api_error" + }, + "screamingSnakeCase": { + "unsafeName": "API_ERROR", + "safeName": "API_ERROR" + }, + "pascalCase": { + "unsafeName": "APIError", + "safeName": "APIError" + } + }, + "wireValue": "API_ERROR" + }, + { + "name": { + "originalName": "AUTHENTICATION_ERROR", + "camelCase": { + "unsafeName": "authenticationError", + "safeName": "authenticationError" + }, + "snakeCase": { + "unsafeName": "authentication_error", + "safeName": "authentication_error" + }, + "screamingSnakeCase": { + "unsafeName": "AUTHENTICATION_ERROR", + "safeName": "AUTHENTICATION_ERROR" + }, + "pascalCase": { + "unsafeName": "AuthenticationError", + "safeName": "AuthenticationError" + } + }, + "wireValue": "AUTHENTICATION_ERROR" + }, + { + "name": { + "originalName": "INVALID_REQUEST_ERROR", + "camelCase": { + "unsafeName": "invalidRequestError", + "safeName": "invalidRequestError" + }, + "snakeCase": { + "unsafeName": "invalid_request_error", + "safeName": "invalid_request_error" + }, + "screamingSnakeCase": { + "unsafeName": "INVALID_REQUEST_ERROR", + "safeName": "INVALID_REQUEST_ERROR" + }, + "pascalCase": { + "unsafeName": "InvalidRequestError", + "safeName": "InvalidRequestError" + } + }, + "wireValue": "INVALID_REQUEST_ERROR" + } + ] + }, + "type_endpoints/put:ErrorCode": { + "type": "enum", + "declaration": { + "name": { + "originalName": "ErrorCode", + "camelCase": { + "unsafeName": "errorCode", + "safeName": "errorCode" + }, + "snakeCase": { + "unsafeName": "error_code", + "safeName": "error_code" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_CODE", + "safeName": "ERROR_CODE" + }, + "pascalCase": { + "unsafeName": "ErrorCode", + "safeName": "ErrorCode" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "values": [ + { + "name": { + "originalName": "INTERNAL_SERVER_ERROR", + "camelCase": { + "unsafeName": "internalServerError", + "safeName": "internalServerError" + }, + "snakeCase": { + "unsafeName": "internal_server_error", + "safeName": "internal_server_error" + }, + "screamingSnakeCase": { + "unsafeName": "INTERNAL_SERVER_ERROR", + "safeName": "INTERNAL_SERVER_ERROR" + }, + "pascalCase": { + "unsafeName": "InternalServerError", + "safeName": "InternalServerError" + } + }, + "wireValue": "INTERNAL_SERVER_ERROR" + }, + { + "name": { + "originalName": "UNAUTHORIZED", + "camelCase": { + "unsafeName": "unauthorized", + "safeName": "unauthorized" + }, + "snakeCase": { + "unsafeName": "unauthorized", + "safeName": "unauthorized" + }, + "screamingSnakeCase": { + "unsafeName": "UNAUTHORIZED", + "safeName": "UNAUTHORIZED" + }, + "pascalCase": { + "unsafeName": "Unauthorized", + "safeName": "Unauthorized" + } + }, + "wireValue": "UNAUTHORIZED" + }, + { + "name": { + "originalName": "FORBIDDEN", + "camelCase": { + "unsafeName": "forbidden", + "safeName": "forbidden" + }, + "snakeCase": { + "unsafeName": "forbidden", + "safeName": "forbidden" + }, + "screamingSnakeCase": { + "unsafeName": "FORBIDDEN", + "safeName": "FORBIDDEN" + }, + "pascalCase": { + "unsafeName": "Forbidden", + "safeName": "Forbidden" + } + }, + "wireValue": "FORBIDDEN" + }, + { + "name": { + "originalName": "BAD_REQUEST", + "camelCase": { + "unsafeName": "badRequest", + "safeName": "badRequest" + }, + "snakeCase": { + "unsafeName": "bad_request", + "safeName": "bad_request" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_REQUEST", + "safeName": "BAD_REQUEST" + }, + "pascalCase": { + "unsafeName": "BadRequest", + "safeName": "BadRequest" + } + }, + "wireValue": "BAD_REQUEST" + }, + { + "name": { + "originalName": "CONFLICT", + "camelCase": { + "unsafeName": "conflict", + "safeName": "conflict" + }, + "snakeCase": { + "unsafeName": "conflict", + "safeName": "conflict" + }, + "screamingSnakeCase": { + "unsafeName": "CONFLICT", + "safeName": "CONFLICT" + }, + "pascalCase": { + "unsafeName": "Conflict", + "safeName": "Conflict" + } + }, + "wireValue": "CONFLICT" + }, + { + "name": { + "originalName": "GONE", + "camelCase": { + "unsafeName": "gone", + "safeName": "gone" + }, + "snakeCase": { + "unsafeName": "gone", + "safeName": "gone" + }, + "screamingSnakeCase": { + "unsafeName": "GONE", + "safeName": "GONE" + }, + "pascalCase": { + "unsafeName": "Gone", + "safeName": "Gone" + } + }, + "wireValue": "GONE" + }, + { + "name": { + "originalName": "UNPROCESSABLE_ENTITY", + "camelCase": { + "unsafeName": "unprocessableEntity", + "safeName": "unprocessableEntity" + }, + "snakeCase": { + "unsafeName": "unprocessable_entity", + "safeName": "unprocessable_entity" + }, + "screamingSnakeCase": { + "unsafeName": "UNPROCESSABLE_ENTITY", + "safeName": "UNPROCESSABLE_ENTITY" + }, + "pascalCase": { + "unsafeName": "UnprocessableEntity", + "safeName": "UnprocessableEntity" + } + }, + "wireValue": "UNPROCESSABLE_ENTITY" + }, + { + "name": { + "originalName": "NOT_IMPLEMENTED", + "camelCase": { + "unsafeName": "notImplemented", + "safeName": "notImplemented" + }, + "snakeCase": { + "unsafeName": "not_implemented", + "safeName": "not_implemented" + }, + "screamingSnakeCase": { + "unsafeName": "NOT_IMPLEMENTED", + "safeName": "NOT_IMPLEMENTED" + }, + "pascalCase": { + "unsafeName": "NotImplemented", + "safeName": "NotImplemented" + } + }, + "wireValue": "NOT_IMPLEMENTED" + }, + { + "name": { + "originalName": "BAD_GATEWAY", + "camelCase": { + "unsafeName": "badGateway", + "safeName": "badGateway" + }, + "snakeCase": { + "unsafeName": "bad_gateway", + "safeName": "bad_gateway" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_GATEWAY", + "safeName": "BAD_GATEWAY" + }, + "pascalCase": { + "unsafeName": "BadGateway", + "safeName": "BadGateway" + } + }, + "wireValue": "BAD_GATEWAY" + }, + { + "name": { + "originalName": "SERVICE_UNAVAILABLE", + "camelCase": { + "unsafeName": "serviceUnavailable", + "safeName": "serviceUnavailable" + }, + "snakeCase": { + "unsafeName": "service_unavailable", + "safeName": "service_unavailable" + }, + "screamingSnakeCase": { + "unsafeName": "SERVICE_UNAVAILABLE", + "safeName": "SERVICE_UNAVAILABLE" + }, + "pascalCase": { + "unsafeName": "ServiceUnavailable", + "safeName": "ServiceUnavailable" + } + }, + "wireValue": "SERVICE_UNAVAILABLE" + }, + { + "name": { + "originalName": "Unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "Unknown" + } + ] + }, + "type_endpoints/put:PutResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "PutResponse", + "camelCase": { + "unsafeName": "putResponse", + "safeName": "putResponse" + }, + "snakeCase": { + "unsafeName": "put_response", + "safeName": "put_response" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_RESPONSE", + "safeName": "PUT_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PutResponse", + "safeName": "PutResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "errors", + "camelCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "snakeCase": { + "unsafeName": "errors", + "safeName": "errors" + }, + "screamingSnakeCase": { + "unsafeName": "ERRORS", + "safeName": "ERRORS" + }, + "pascalCase": { + "unsafeName": "Errors", + "safeName": "Errors" + } + }, + "wireValue": "errors" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "list", + "value": { + "type": "named", + "value": "type_endpoints/put:Error" + } + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_general-errors:BadObjectRequestInfo": { + "type": "object", + "declaration": { + "name": { + "originalName": "BadObjectRequestInfo", + "camelCase": { + "unsafeName": "badObjectRequestInfo", + "safeName": "badObjectRequestInfo" + }, + "snakeCase": { + "unsafeName": "bad_object_request_info", + "safeName": "bad_object_request_info" + }, + "screamingSnakeCase": { + "unsafeName": "BAD_OBJECT_REQUEST_INFO", + "safeName": "BAD_OBJECT_REQUEST_INFO" + }, + "pascalCase": { + "unsafeName": "BadObjectRequestInfo", + "safeName": "BadObjectRequestInfo" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "message", + "camelCase": { + "unsafeName": "message", + "safeName": "message" + }, + "snakeCase": { + "unsafeName": "message", + "safeName": "message" + }, + "screamingSnakeCase": { + "unsafeName": "MESSAGE", + "safeName": "MESSAGE" + }, + "pascalCase": { + "unsafeName": "Message", + "safeName": "Message" + } + }, + "wireValue": "message" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/docs:ObjectWithDocs": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithDocs", + "camelCase": { + "unsafeName": "objectWithDocs", + "safeName": "objectWithDocs" + }, + "snakeCase": { + "unsafeName": "object_with_docs", + "safeName": "object_with_docs" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DOCS", + "safeName": "OBJECT_WITH_DOCS" + }, + "pascalCase": { + "unsafeName": "ObjectWithDocs", + "safeName": "ObjectWithDocs" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/enum:WeatherReport": { + "type": "enum", + "declaration": { + "name": { + "originalName": "WeatherReport", + "camelCase": { + "unsafeName": "weatherReport", + "safeName": "weatherReport" + }, + "snakeCase": { + "unsafeName": "weather_report", + "safeName": "weather_report" + }, + "screamingSnakeCase": { + "unsafeName": "WEATHER_REPORT", + "safeName": "WEATHER_REPORT" + }, + "pascalCase": { + "unsafeName": "WeatherReport", + "safeName": "WeatherReport" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + } + }, + "values": [ + { + "name": { + "originalName": "SUNNY", + "camelCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "snakeCase": { + "unsafeName": "sunny", + "safeName": "sunny" + }, + "screamingSnakeCase": { + "unsafeName": "SUNNY", + "safeName": "SUNNY" + }, + "pascalCase": { + "unsafeName": "Sunny", + "safeName": "Sunny" + } + }, + "wireValue": "SUNNY" + }, + { + "name": { + "originalName": "CLOUDY", + "camelCase": { + "unsafeName": "cloudy", + "safeName": "cloudy" + }, + "snakeCase": { + "unsafeName": "cloudy", + "safeName": "cloudy" + }, + "screamingSnakeCase": { + "unsafeName": "CLOUDY", + "safeName": "CLOUDY" + }, + "pascalCase": { + "unsafeName": "Cloudy", + "safeName": "Cloudy" + } + }, + "wireValue": "CLOUDY" + }, + { + "name": { + "originalName": "RAINING", + "camelCase": { + "unsafeName": "raining", + "safeName": "raining" + }, + "snakeCase": { + "unsafeName": "raining", + "safeName": "raining" + }, + "screamingSnakeCase": { + "unsafeName": "RAINING", + "safeName": "RAINING" + }, + "pascalCase": { + "unsafeName": "Raining", + "safeName": "Raining" + } + }, + "wireValue": "RAINING" + }, + { + "name": { + "originalName": "SNOWING", + "camelCase": { + "unsafeName": "snowing", + "safeName": "snowing" + }, + "snakeCase": { + "unsafeName": "snowing", + "safeName": "snowing" + }, + "screamingSnakeCase": { + "unsafeName": "SNOWING", + "safeName": "SNOWING" + }, + "pascalCase": { + "unsafeName": "Snowing", + "safeName": "Snowing" + } + }, + "wireValue": "SNOWING" + } + ] + }, + "type_types/object:ObjectWithOptionalField": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithOptionalField", + "camelCase": { + "unsafeName": "objectWithOptionalField", + "safeName": "objectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "object_with_optional_field", + "safeName": "object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithOptionalField", + "safeName": "ObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "long", + "camelCase": { + "unsafeName": "long", + "safeName": "long" + }, + "snakeCase": { + "unsafeName": "long", + "safeName": "long" + }, + "screamingSnakeCase": { + "unsafeName": "LONG", + "safeName": "LONG" + }, + "pascalCase": { + "unsafeName": "Long", + "safeName": "Long" + } + }, + "wireValue": "long" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "LONG" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "double", + "camelCase": { + "unsafeName": "double", + "safeName": "double" + }, + "snakeCase": { + "unsafeName": "double", + "safeName": "double" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE", + "safeName": "DOUBLE" + }, + "pascalCase": { + "unsafeName": "Double", + "safeName": "Double" + } + }, + "wireValue": "double" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "DOUBLE" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "bool", + "camelCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "snakeCase": { + "unsafeName": "bool", + "safeName": "bool" + }, + "screamingSnakeCase": { + "unsafeName": "BOOL", + "safeName": "BOOL" + }, + "pascalCase": { + "unsafeName": "Bool", + "safeName": "Bool" + } + }, + "wireValue": "bool" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "datetime", + "camelCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "snakeCase": { + "unsafeName": "datetime", + "safeName": "datetime" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME", + "safeName": "DATETIME" + }, + "pascalCase": { + "unsafeName": "Datetime", + "safeName": "Datetime" + } + }, + "wireValue": "datetime" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "DATE_TIME" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "date", + "camelCase": { + "unsafeName": "date", + "safeName": "date" + }, + "snakeCase": { + "unsafeName": "date", + "safeName": "date" + }, + "screamingSnakeCase": { + "unsafeName": "DATE", + "safeName": "DATE" + }, + "pascalCase": { + "unsafeName": "Date", + "safeName": "Date" + } + }, + "wireValue": "date" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "DATE" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "uuid", + "camelCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "snakeCase": { + "unsafeName": "uuid", + "safeName": "uuid" + }, + "screamingSnakeCase": { + "unsafeName": "UUID", + "safeName": "UUID" + }, + "pascalCase": { + "unsafeName": "UUID", + "safeName": "UUID" + } + }, + "wireValue": "uuid" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "UUID" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "base64", + "camelCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "snakeCase": { + "unsafeName": "base64", + "safeName": "base64" + }, + "screamingSnakeCase": { + "unsafeName": "BASE64", + "safeName": "BASE64" + }, + "pascalCase": { + "unsafeName": "Base64", + "safeName": "Base64" + } + }, + "wireValue": "base64" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BASE_64" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "wireValue": "list" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "set", + "camelCase": { + "unsafeName": "set", + "safeName": "set" + }, + "snakeCase": { + "unsafeName": "set", + "safeName": "set" + }, + "screamingSnakeCase": { + "unsafeName": "SET", + "safeName": "SET" + }, + "pascalCase": { + "unsafeName": "Set", + "safeName": "Set" + } + }, + "wireValue": "set" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "set", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "INTEGER" + }, + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "bigint", + "camelCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "snakeCase": { + "unsafeName": "bigint", + "safeName": "bigint" + }, + "screamingSnakeCase": { + "unsafeName": "BIGINT", + "safeName": "BIGINT" + }, + "pascalCase": { + "unsafeName": "Bigint", + "safeName": "Bigint" + } + }, + "wireValue": "bigint" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BIG_INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:ObjectWithRequiredField": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithRequiredField", + "camelCase": { + "unsafeName": "objectWithRequiredField", + "safeName": "objectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "object_with_required_field", + "safeName": "object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_REQUIRED_FIELD", + "safeName": "OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithRequiredField", + "safeName": "ObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:ObjectWithMapOfMap": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithMapOfMap", + "camelCase": { + "unsafeName": "objectWithMapOfMap", + "safeName": "objectWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "object_with_map_of_map", + "safeName": "object_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_MAP_OF_MAP", + "safeName": "OBJECT_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "ObjectWithMapOfMap", + "safeName": "ObjectWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "map", + "camelCase": { + "unsafeName": "map", + "safeName": "map" + }, + "snakeCase": { + "unsafeName": "map", + "safeName": "map" + }, + "screamingSnakeCase": { + "unsafeName": "MAP", + "safeName": "MAP" + }, + "pascalCase": { + "unsafeName": "Map", + "safeName": "Map" + } + }, + "wireValue": "map" + }, + "typeReference": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:NestedObjectWithOptionalField": { + "type": "object", + "declaration": { + "name": { + "originalName": "NestedObjectWithOptionalField", + "camelCase": { + "unsafeName": "nestedObjectWithOptionalField", + "safeName": "nestedObjectWithOptionalField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_optional_field", + "safeName": "nested_object_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD", + "safeName": "NESTED_OBJECT_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithOptionalField", + "safeName": "NestedObjectWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:NestedObjectWithRequiredField": { + "type": "object", + "declaration": { + "name": { + "originalName": "NestedObjectWithRequiredField", + "camelCase": { + "unsafeName": "nestedObjectWithRequiredField", + "safeName": "nestedObjectWithRequiredField" + }, + "snakeCase": { + "unsafeName": "nested_object_with_required_field", + "safeName": "nested_object_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD", + "safeName": "NESTED_OBJECT_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "NestedObjectWithRequiredField", + "safeName": "NestedObjectWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "typeReference": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:DoubleOptional": { + "type": "object", + "declaration": { + "name": { + "originalName": "DoubleOptional", + "camelCase": { + "unsafeName": "doubleOptional", + "safeName": "doubleOptional" + }, + "snakeCase": { + "unsafeName": "double_optional", + "safeName": "double_optional" + }, + "screamingSnakeCase": { + "unsafeName": "DOUBLE_OPTIONAL", + "safeName": "DOUBLE_OPTIONAL" + }, + "pascalCase": { + "unsafeName": "DoubleOptional", + "safeName": "DoubleOptional" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "optionalAlias", + "camelCase": { + "unsafeName": "optionalAlias", + "safeName": "optionalAlias" + }, + "snakeCase": { + "unsafeName": "optional_alias", + "safeName": "optional_alias" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_ALIAS", + "safeName": "OPTIONAL_ALIAS" + }, + "pascalCase": { + "unsafeName": "OptionalAlias", + "safeName": "OptionalAlias" + } + }, + "wireValue": "optionalAlias" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "named", + "value": "type_types/object:OptionalAlias" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:OptionalAlias": { + "type": "alias", + "declaration": { + "name": { + "originalName": "OptionalAlias", + "camelCase": { + "unsafeName": "optionalAlias", + "safeName": "optionalAlias" + }, + "snakeCase": { + "unsafeName": "optional_alias", + "safeName": "optional_alias" + }, + "screamingSnakeCase": { + "unsafeName": "OPTIONAL_ALIAS", + "safeName": "OPTIONAL_ALIAS" + }, + "pascalCase": { + "unsafeName": "OptionalAlias", + "safeName": "OptionalAlias" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "type_types/object:ObjectWithDatetimeLikeString": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithDatetimeLikeString", + "camelCase": { + "unsafeName": "objectWithDatetimeLikeString", + "safeName": "objectWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "object_with_datetime_like_string", + "safeName": "object_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_DATETIME_LIKE_STRING", + "safeName": "OBJECT_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "ObjectWithDatetimeLikeString", + "safeName": "ObjectWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "datetimeLikeString", + "camelCase": { + "unsafeName": "datetimeLikeString", + "safeName": "datetimeLikeString" + }, + "snakeCase": { + "unsafeName": "datetime_like_string", + "safeName": "datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "DATETIME_LIKE_STRING", + "safeName": "DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "DatetimeLikeString", + "safeName": "DatetimeLikeString" + } + }, + "wireValue": "datetimeLikeString" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "actualDatetime", + "camelCase": { + "unsafeName": "actualDatetime", + "safeName": "actualDatetime" + }, + "snakeCase": { + "unsafeName": "actual_datetime", + "safeName": "actual_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "ACTUAL_DATETIME", + "safeName": "ACTUAL_DATETIME" + }, + "pascalCase": { + "unsafeName": "ActualDatetime", + "safeName": "ActualDatetime" + } + }, + "wireValue": "actualDatetime" + }, + "typeReference": { + "type": "primitive", + "value": "DATE_TIME" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/object:ObjectWithUnknownField": { + "type": "object", + "declaration": { + "name": { + "originalName": "ObjectWithUnknownField", + "camelCase": { + "unsafeName": "objectWithUnknownField", + "safeName": "objectWithUnknownField" + }, + "snakeCase": { + "unsafeName": "object_with_unknown_field", + "safeName": "object_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT_WITH_UNKNOWN_FIELD", + "safeName": "OBJECT_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "ObjectWithUnknownField", + "safeName": "ObjectWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "unknown", + "camelCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "snakeCase": { + "unsafeName": "unknown", + "safeName": "unknown" + }, + "screamingSnakeCase": { + "unsafeName": "UNKNOWN", + "safeName": "UNKNOWN" + }, + "pascalCase": { + "unsafeName": "Unknown", + "safeName": "Unknown" + } + }, + "wireValue": "unknown" + }, + "typeReference": { + "type": "unknown" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/union:Animal": { + "type": "discriminatedUnion", + "declaration": { + "name": { + "originalName": "Animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "discriminant": { + "name": { + "originalName": "animal", + "camelCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "snakeCase": { + "unsafeName": "animal", + "safeName": "animal" + }, + "screamingSnakeCase": { + "unsafeName": "ANIMAL", + "safeName": "ANIMAL" + }, + "pascalCase": { + "unsafeName": "Animal", + "safeName": "Animal" + } + }, + "wireValue": "animal" + }, + "types": { + "dog": { + "type": "samePropertiesAsObject", + "typeId": "type_types/union:Dog", + "discriminantValue": { + "name": { + "originalName": "dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "wireValue": "dog" + }, + "properties": [] + }, + "cat": { + "type": "samePropertiesAsObject", + "typeId": "type_types/union:Cat", + "discriminantValue": { + "name": { + "originalName": "cat", + "camelCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "snakeCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "screamingSnakeCase": { + "unsafeName": "CAT", + "safeName": "CAT" + }, + "pascalCase": { + "unsafeName": "Cat", + "safeName": "Cat" + } + }, + "wireValue": "cat" + }, + "properties": [] + } + } + }, + "type_types/union:Dog": { + "type": "object", + "declaration": { + "name": { + "originalName": "Dog", + "camelCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "snakeCase": { + "unsafeName": "dog", + "safeName": "dog" + }, + "screamingSnakeCase": { + "unsafeName": "DOG", + "safeName": "DOG" + }, + "pascalCase": { + "unsafeName": "Dog", + "safeName": "Dog" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "likesToWoof", + "camelCase": { + "unsafeName": "likesToWoof", + "safeName": "likesToWoof" + }, + "snakeCase": { + "unsafeName": "likes_to_woof", + "safeName": "likes_to_woof" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_WOOF", + "safeName": "LIKES_TO_WOOF" + }, + "pascalCase": { + "unsafeName": "LikesToWoof", + "safeName": "LikesToWoof" + } + }, + "wireValue": "likesToWoof" + }, + "typeReference": { + "type": "primitive", + "value": "BOOLEAN" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/union:Cat": { + "type": "object", + "declaration": { + "name": { + "originalName": "Cat", + "camelCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "snakeCase": { + "unsafeName": "cat", + "safeName": "cat" + }, + "screamingSnakeCase": { + "unsafeName": "CAT", + "safeName": "CAT" + }, + "pascalCase": { + "unsafeName": "Cat", + "safeName": "Cat" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "likesToMeow", + "camelCase": { + "unsafeName": "likesToMeow", + "safeName": "likesToMeow" + }, + "snakeCase": { + "unsafeName": "likes_to_meow", + "safeName": "likes_to_meow" + }, + "screamingSnakeCase": { + "unsafeName": "LIKES_TO_MEOW", + "safeName": "LIKES_TO_MEOW" + }, + "pascalCase": { + "unsafeName": "LikesToMeow", + "safeName": "LikesToMeow" + } + }, + "wireValue": "likesToMeow" + }, + "typeReference": { + "type": "primitive", + "value": "BOOLEAN" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_types/union:MixedType": { + "type": "undiscriminatedUnion", + "declaration": { + "name": { + "originalName": "MixedType", + "camelCase": { + "unsafeName": "mixedType", + "safeName": "mixedType" + }, + "snakeCase": { + "unsafeName": "mixed_type", + "safeName": "mixed_type" + }, + "screamingSnakeCase": { + "unsafeName": "MIXED_TYPE", + "safeName": "MIXED_TYPE" + }, + "pascalCase": { + "unsafeName": "MixedType", + "safeName": "MixedType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "types": [ + { + "type": "primitive", + "value": "DOUBLE" + }, + { + "type": "primitive", + "value": "BOOLEAN" + }, + { + "type": "primitive", + "value": "STRING" + }, + { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + } + ] + } + }, + "headers": [], + "endpoints": { + "endpoint_endpoints/container.getAndReturnListOfPrimitives": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnListOfPrimitives", + "camelCase": { + "unsafeName": "getAndReturnListOfPrimitives", + "safeName": "getAndReturnListOfPrimitives" + }, + "snakeCase": { + "unsafeName": "get_and_return_list_of_primitives", + "safeName": "get_and_return_list_of_primitives" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LIST_OF_PRIMITIVES", + "safeName": "GET_AND_RETURN_LIST_OF_PRIMITIVES" + }, + "pascalCase": { + "unsafeName": "GetAndReturnListOfPrimitives", + "safeName": "GetAndReturnListOfPrimitives" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/list-of-primitives" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnListOfObjects": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnListOfObjects", + "camelCase": { + "unsafeName": "getAndReturnListOfObjects", + "safeName": "getAndReturnListOfObjects" + }, + "snakeCase": { + "unsafeName": "get_and_return_list_of_objects", + "safeName": "get_and_return_list_of_objects" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LIST_OF_OBJECTS", + "safeName": "GET_AND_RETURN_LIST_OF_OBJECTS" + }, + "pascalCase": { + "unsafeName": "GetAndReturnListOfObjects", + "safeName": "GetAndReturnListOfObjects" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/list-of-objects" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "list", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnSetOfPrimitives": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnSetOfPrimitives", + "camelCase": { + "unsafeName": "getAndReturnSetOfPrimitives", + "safeName": "getAndReturnSetOfPrimitives" + }, + "snakeCase": { + "unsafeName": "get_and_return_set_of_primitives", + "safeName": "get_and_return_set_of_primitives" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_SET_OF_PRIMITIVES", + "safeName": "GET_AND_RETURN_SET_OF_PRIMITIVES" + }, + "pascalCase": { + "unsafeName": "GetAndReturnSetOfPrimitives", + "safeName": "GetAndReturnSetOfPrimitives" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/set-of-primitives" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "set", + "value": { + "type": "primitive", + "value": "STRING" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnSetOfObjects": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnSetOfObjects", + "camelCase": { + "unsafeName": "getAndReturnSetOfObjects", + "safeName": "getAndReturnSetOfObjects" + }, + "snakeCase": { + "unsafeName": "get_and_return_set_of_objects", + "safeName": "get_and_return_set_of_objects" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_SET_OF_OBJECTS", + "safeName": "GET_AND_RETURN_SET_OF_OBJECTS" + }, + "pascalCase": { + "unsafeName": "GetAndReturnSetOfObjects", + "safeName": "GetAndReturnSetOfObjects" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/set-of-objects" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "set", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnMapPrimToPrim": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnMapPrimToPrim", + "camelCase": { + "unsafeName": "getAndReturnMapPrimToPrim", + "safeName": "getAndReturnMapPrimToPrim" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_prim_to_prim", + "safeName": "get_and_return_map_prim_to_prim" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_PRIM_TO_PRIM", + "safeName": "GET_AND_RETURN_MAP_PRIM_TO_PRIM" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapPrimToPrim", + "safeName": "GetAndReturnMapPrimToPrim" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/map-prim-to-prim" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "primitive", + "value": "STRING" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnMapOfPrimToObject": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnMapOfPrimToObject", + "camelCase": { + "unsafeName": "getAndReturnMapOfPrimToObject", + "safeName": "getAndReturnMapOfPrimToObject" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_of_prim_to_object", + "safeName": "get_and_return_map_of_prim_to_object" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_OBJECT", + "safeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_OBJECT" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapOfPrimToObject", + "safeName": "GetAndReturnMapOfPrimToObject" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/map-prim-to-object" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnMapOfPrimToUndiscriminatedUnion": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnMapOfPrimToUndiscriminatedUnion", + "camelCase": { + "unsafeName": "getAndReturnMapOfPrimToUndiscriminatedUnion", + "safeName": "getAndReturnMapOfPrimToUndiscriminatedUnion" + }, + "snakeCase": { + "unsafeName": "get_and_return_map_of_prim_to_undiscriminated_union", + "safeName": "get_and_return_map_of_prim_to_undiscriminated_union" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_UNDISCRIMINATED_UNION", + "safeName": "GET_AND_RETURN_MAP_OF_PRIM_TO_UNDISCRIMINATED_UNION" + }, + "pascalCase": { + "unsafeName": "GetAndReturnMapOfPrimToUndiscriminatedUnion", + "safeName": "GetAndReturnMapOfPrimToUndiscriminatedUnion" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/map-prim-to-union" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "map", + "key": { + "type": "primitive", + "value": "STRING" + }, + "value": { + "type": "named", + "value": "type_types/union:MixedType" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/container.getAndReturnOptional": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnOptional", + "camelCase": { + "unsafeName": "getAndReturnOptional", + "safeName": "getAndReturnOptional" + }, + "snakeCase": { + "unsafeName": "get_and_return_optional", + "safeName": "get_and_return_optional" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_OPTIONAL", + "safeName": "GET_AND_RETURN_OPTIONAL" + }, + "pascalCase": { + "unsafeName": "GetAndReturnOptional", + "safeName": "GetAndReturnOptional" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + } + }, + "location": { + "method": "POST", + "path": "/container/opt-objects" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "optional", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/content-type.postJsonPatchContentType": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postJsonPatchContentType", + "camelCase": { + "unsafeName": "postJSONPatchContentType", + "safeName": "postJSONPatchContentType" + }, + "snakeCase": { + "unsafeName": "post_json_patch_content_type", + "safeName": "post_json_patch_content_type" + }, + "screamingSnakeCase": { + "unsafeName": "POST_JSON_PATCH_CONTENT_TYPE", + "safeName": "POST_JSON_PATCH_CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "PostJSONPatchContentType", + "safeName": "PostJSONPatchContentType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + } + }, + "location": { + "method": "POST", + "path": "/foo/bar" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/content-type.postJsonPatchContentWithCharsetType": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postJsonPatchContentWithCharsetType", + "camelCase": { + "unsafeName": "postJSONPatchContentWithCharsetType", + "safeName": "postJSONPatchContentWithCharsetType" + }, + "snakeCase": { + "unsafeName": "post_json_patch_content_with_charset_type", + "safeName": "post_json_patch_content_with_charset_type" + }, + "screamingSnakeCase": { + "unsafeName": "POST_JSON_PATCH_CONTENT_WITH_CHARSET_TYPE", + "safeName": "POST_JSON_PATCH_CONTENT_WITH_CHARSET_TYPE" + }, + "pascalCase": { + "unsafeName": "PostJSONPatchContentWithCharsetType", + "safeName": "PostJSONPatchContentWithCharsetType" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + } + }, + "location": { + "method": "POST", + "path": "/foo/baz" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-a.create": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "location": { + "method": "POST", + "path": "/duplicate-names-a" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateRequestA", + "camelCase": { + "unsafeName": "createRequestA", + "safeName": "createRequestA" + }, + "snakeCase": { + "unsafeName": "create_request_a", + "safeName": "create_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_A", + "safeName": "CREATE_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "CreateRequestA", + "safeName": "CreateRequestA" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "name", + "camelCase": { + "unsafeName": "name", + "safeName": "name" + }, + "snakeCase": { + "unsafeName": "name", + "safeName": "name" + }, + "screamingSnakeCase": { + "unsafeName": "NAME", + "safeName": "NAME" + }, + "pascalCase": { + "unsafeName": "Name", + "safeName": "Name" + } + }, + "wireValue": "name" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "value", + "camelCase": { + "unsafeName": "value", + "safeName": "value" + }, + "snakeCase": { + "unsafeName": "value", + "safeName": "value" + }, + "screamingSnakeCase": { + "unsafeName": "VALUE", + "safeName": "VALUE" + }, + "pascalCase": { + "unsafeName": "Value", + "safeName": "Value" + } + }, + "wireValue": "value" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-a.get": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-a/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetRequestA", + "camelCase": { + "unsafeName": "getRequestA", + "safeName": "getRequestA" + }, + "snakeCase": { + "unsafeName": "get_request_a", + "safeName": "get_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_A", + "safeName": "GET_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "GetRequestA", + "safeName": "GetRequestA" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "filter", + "camelCase": { + "unsafeName": "filter", + "safeName": "filter" + }, + "snakeCase": { + "unsafeName": "filter", + "safeName": "filter" + }, + "screamingSnakeCase": { + "unsafeName": "FILTER", + "safeName": "FILTER" + }, + "pascalCase": { + "unsafeName": "Filter", + "safeName": "Filter" + } + }, + "wireValue": "filter" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-a.list": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-a" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListRequestA", + "camelCase": { + "unsafeName": "listRequestA", + "safeName": "listRequestA" + }, + "snakeCase": { + "unsafeName": "list_request_a", + "safeName": "list_request_a" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_A", + "safeName": "LIST_REQUEST_A" + }, + "pascalCase": { + "unsafeName": "ListRequestA", + "safeName": "ListRequestA" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "page", + "camelCase": { + "unsafeName": "page", + "safeName": "page" + }, + "snakeCase": { + "unsafeName": "page", + "safeName": "page" + }, + "screamingSnakeCase": { + "unsafeName": "PAGE", + "safeName": "PAGE" + }, + "pascalCase": { + "unsafeName": "Page", + "safeName": "Page" + } + }, + "wireValue": "page" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-b.create": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "location": { + "method": "POST", + "path": "/duplicate-names-b" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateRequestB", + "camelCase": { + "unsafeName": "createRequestB", + "safeName": "createRequestB" + }, + "snakeCase": { + "unsafeName": "create_request_b", + "safeName": "create_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_B", + "safeName": "CREATE_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "CreateRequestB", + "safeName": "CreateRequestB" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "description", + "camelCase": { + "unsafeName": "description", + "safeName": "description" + }, + "snakeCase": { + "unsafeName": "description", + "safeName": "description" + }, + "screamingSnakeCase": { + "unsafeName": "DESCRIPTION", + "safeName": "DESCRIPTION" + }, + "pascalCase": { + "unsafeName": "Description", + "safeName": "Description" + } + }, + "wireValue": "description" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "count", + "camelCase": { + "unsafeName": "count", + "safeName": "count" + }, + "snakeCase": { + "unsafeName": "count", + "safeName": "count" + }, + "screamingSnakeCase": { + "unsafeName": "COUNT", + "safeName": "COUNT" + }, + "pascalCase": { + "unsafeName": "Count", + "safeName": "Count" + } + }, + "wireValue": "count" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-b.get": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-b/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetRequestB", + "camelCase": { + "unsafeName": "getRequestB", + "safeName": "getRequestB" + }, + "snakeCase": { + "unsafeName": "get_request_b", + "safeName": "get_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_B", + "safeName": "GET_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "GetRequestB", + "safeName": "GetRequestB" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "expand", + "camelCase": { + "unsafeName": "expand", + "safeName": "expand" + }, + "snakeCase": { + "unsafeName": "expand", + "safeName": "expand" + }, + "screamingSnakeCase": { + "unsafeName": "EXPAND", + "safeName": "EXPAND" + }, + "pascalCase": { + "unsafeName": "Expand", + "safeName": "Expand" + } + }, + "wireValue": "expand" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-b.list": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-b" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListRequestB", + "camelCase": { + "unsafeName": "listRequestB", + "safeName": "listRequestB" + }, + "snakeCase": { + "unsafeName": "list_request_b", + "safeName": "list_request_b" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_B", + "safeName": "LIST_REQUEST_B" + }, + "pascalCase": { + "unsafeName": "ListRequestB", + "safeName": "ListRequestB" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "cursor", + "camelCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "snakeCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "screamingSnakeCase": { + "unsafeName": "CURSOR", + "safeName": "CURSOR" + }, + "pascalCase": { + "unsafeName": "Cursor", + "safeName": "Cursor" + } + }, + "wireValue": "cursor" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "size", + "camelCase": { + "unsafeName": "size", + "safeName": "size" + }, + "snakeCase": { + "unsafeName": "size", + "safeName": "size" + }, + "screamingSnakeCase": { + "unsafeName": "SIZE", + "safeName": "SIZE" + }, + "pascalCase": { + "unsafeName": "Size", + "safeName": "Size" + } + }, + "wireValue": "size" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-c.create": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "create", + "camelCase": { + "unsafeName": "create", + "safeName": "create" + }, + "snakeCase": { + "unsafeName": "create", + "safeName": "create" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE", + "safeName": "CREATE" + }, + "pascalCase": { + "unsafeName": "Create", + "safeName": "Create" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "location": { + "method": "POST", + "path": "/duplicate-names-c" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "CreateRequestC", + "camelCase": { + "unsafeName": "createRequestC", + "safeName": "createRequestC" + }, + "snakeCase": { + "unsafeName": "create_request_c", + "safeName": "create_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "CREATE_REQUEST_C", + "safeName": "CREATE_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "CreateRequestC", + "safeName": "CreateRequestC" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "label", + "camelCase": { + "unsafeName": "label", + "safeName": "label" + }, + "snakeCase": { + "unsafeName": "label", + "safeName": "label" + }, + "screamingSnakeCase": { + "unsafeName": "LABEL", + "safeName": "LABEL" + }, + "pascalCase": { + "unsafeName": "Label", + "safeName": "Label" + } + }, + "wireValue": "label" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "priority", + "camelCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "snakeCase": { + "unsafeName": "priority", + "safeName": "priority" + }, + "screamingSnakeCase": { + "unsafeName": "PRIORITY", + "safeName": "PRIORITY" + }, + "pascalCase": { + "unsafeName": "Priority", + "safeName": "Priority" + } + }, + "wireValue": "priority" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-c.get": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "get", + "camelCase": { + "unsafeName": "get", + "safeName": "get" + }, + "snakeCase": { + "unsafeName": "get", + "safeName": "get" + }, + "screamingSnakeCase": { + "unsafeName": "GET", + "safeName": "GET" + }, + "pascalCase": { + "unsafeName": "Get", + "safeName": "Get" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-c/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetRequestC", + "camelCase": { + "unsafeName": "getRequestC", + "safeName": "getRequestC" + }, + "snakeCase": { + "unsafeName": "get_request_c", + "safeName": "get_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "GET_REQUEST_C", + "safeName": "GET_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "GetRequestC", + "safeName": "GetRequestC" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "verbose", + "camelCase": { + "unsafeName": "verbose", + "safeName": "verbose" + }, + "snakeCase": { + "unsafeName": "verbose", + "safeName": "verbose" + }, + "screamingSnakeCase": { + "unsafeName": "VERBOSE", + "safeName": "VERBOSE" + }, + "pascalCase": { + "unsafeName": "Verbose", + "safeName": "Verbose" + } + }, + "wireValue": "verbose" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/duplicate-names-c.list": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "list", + "camelCase": { + "unsafeName": "list", + "safeName": "list" + }, + "snakeCase": { + "unsafeName": "list", + "safeName": "list" + }, + "screamingSnakeCase": { + "unsafeName": "LIST", + "safeName": "LIST" + }, + "pascalCase": { + "unsafeName": "List", + "safeName": "List" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "location": { + "method": "GET", + "path": "/duplicate-names-c" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListRequestC", + "camelCase": { + "unsafeName": "listRequestC", + "safeName": "listRequestC" + }, + "snakeCase": { + "unsafeName": "list_request_c", + "safeName": "list_request_c" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_REQUEST_C", + "safeName": "LIST_REQUEST_C" + }, + "pascalCase": { + "unsafeName": "ListRequestC", + "safeName": "ListRequestC" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "offset", + "camelCase": { + "unsafeName": "offset", + "safeName": "offset" + }, + "snakeCase": { + "unsafeName": "offset", + "safeName": "offset" + }, + "screamingSnakeCase": { + "unsafeName": "OFFSET", + "safeName": "OFFSET" + }, + "pascalCase": { + "unsafeName": "Offset", + "safeName": "Offset" + } + }, + "wireValue": "offset" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "count", + "camelCase": { + "unsafeName": "count", + "safeName": "count" + }, + "snakeCase": { + "unsafeName": "count", + "safeName": "count" + }, + "screamingSnakeCase": { + "unsafeName": "COUNT", + "safeName": "COUNT" + }, + "pascalCase": { + "unsafeName": "Count", + "safeName": "Count" + } + }, + "wireValue": "count" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/enum.getAndReturnEnum": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnEnum", + "camelCase": { + "unsafeName": "getAndReturnEnum", + "safeName": "getAndReturnEnum" + }, + "snakeCase": { + "unsafeName": "get_and_return_enum", + "safeName": "get_and_return_enum" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_ENUM", + "safeName": "GET_AND_RETURN_ENUM" + }, + "pascalCase": { + "unsafeName": "GetAndReturnEnum", + "safeName": "GetAndReturnEnum" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + } + }, + "location": { + "method": "POST", + "path": "/enum" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/enum:WeatherReport" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testGet": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testGet", + "camelCase": { + "unsafeName": "testGet", + "safeName": "testGet" + }, + "snakeCase": { + "unsafeName": "test_get", + "safeName": "test_get" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_GET", + "safeName": "TEST_GET" + }, + "pascalCase": { + "unsafeName": "TestGet", + "safeName": "TestGet" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "GET", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testPost": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testPost", + "camelCase": { + "unsafeName": "testPost", + "safeName": "testPost" + }, + "snakeCase": { + "unsafeName": "test_post", + "safeName": "test_post" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_POST", + "safeName": "TEST_POST" + }, + "pascalCase": { + "unsafeName": "TestPost", + "safeName": "TestPost" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "POST", + "path": "/http-methods" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testPut": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testPut", + "camelCase": { + "unsafeName": "testPut", + "safeName": "testPut" + }, + "snakeCase": { + "unsafeName": "test_put", + "safeName": "test_put" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_PUT", + "safeName": "TEST_PUT" + }, + "pascalCase": { + "unsafeName": "TestPut", + "safeName": "TestPut" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testPatch": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testPatch", + "camelCase": { + "unsafeName": "testPatch", + "safeName": "testPatch" + }, + "snakeCase": { + "unsafeName": "test_patch", + "safeName": "test_patch" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_PATCH", + "safeName": "TEST_PATCH" + }, + "pascalCase": { + "unsafeName": "TestPatch", + "safeName": "TestPatch" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "PATCH", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/http-methods.testDelete": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "testDelete", + "camelCase": { + "unsafeName": "testDelete", + "safeName": "testDelete" + }, + "snakeCase": { + "unsafeName": "test_delete", + "safeName": "test_delete" + }, + "screamingSnakeCase": { + "unsafeName": "TEST_DELETE", + "safeName": "TEST_DELETE" + }, + "pascalCase": { + "unsafeName": "TestDelete", + "safeName": "TestDelete" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + } + }, + "location": { + "method": "DELETE", + "path": "/http-methods/{id}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithOptionalField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithOptionalField", + "camelCase": { + "unsafeName": "getAndReturnWithOptionalField", + "safeName": "getAndReturnWithOptionalField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_optional_field", + "safeName": "get_and_return_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_OPTIONAL_FIELD", + "safeName": "GET_AND_RETURN_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithOptionalField", + "safeName": "GetAndReturnWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-optional-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithRequiredField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithRequiredField", + "camelCase": { + "unsafeName": "getAndReturnWithRequiredField", + "safeName": "getAndReturnWithRequiredField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_required_field", + "safeName": "get_and_return_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_REQUIRED_FIELD", + "safeName": "GET_AND_RETURN_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithRequiredField", + "safeName": "GetAndReturnWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-required-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithMapOfMap": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithMapOfMap", + "camelCase": { + "unsafeName": "getAndReturnWithMapOfMap", + "safeName": "getAndReturnWithMapOfMap" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_map_of_map", + "safeName": "get_and_return_with_map_of_map" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_MAP_OF_MAP", + "safeName": "GET_AND_RETURN_WITH_MAP_OF_MAP" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithMapOfMap", + "safeName": "GetAndReturnWithMapOfMap" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-map-of-map" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithMapOfMap" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnNestedWithOptionalField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnNestedWithOptionalField", + "camelCase": { + "unsafeName": "getAndReturnNestedWithOptionalField", + "safeName": "getAndReturnNestedWithOptionalField" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_optional_field", + "safeName": "get_and_return_nested_with_optional_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_OPTIONAL_FIELD", + "safeName": "GET_AND_RETURN_NESTED_WITH_OPTIONAL_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithOptionalField", + "safeName": "GetAndReturnNestedWithOptionalField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-nested-with-optional-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:NestedObjectWithOptionalField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnNestedWithRequiredField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnNestedWithRequiredField", + "camelCase": { + "unsafeName": "getAndReturnNestedWithRequiredField", + "safeName": "getAndReturnNestedWithRequiredField" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_required_field", + "safeName": "get_and_return_nested_with_required_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD", + "safeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithRequiredField", + "safeName": "GetAndReturnNestedWithRequiredField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-nested-with-required-field/{string}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:NestedObjectWithRequiredField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnNestedWithRequiredFieldAsList": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnNestedWithRequiredFieldAsList", + "camelCase": { + "unsafeName": "getAndReturnNestedWithRequiredFieldAsList", + "safeName": "getAndReturnNestedWithRequiredFieldAsList" + }, + "snakeCase": { + "unsafeName": "get_and_return_nested_with_required_field_as_list", + "safeName": "get_and_return_nested_with_required_field_as_list" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD_AS_LIST", + "safeName": "GET_AND_RETURN_NESTED_WITH_REQUIRED_FIELD_AS_LIST" + }, + "pascalCase": { + "unsafeName": "GetAndReturnNestedWithRequiredFieldAsList", + "safeName": "GetAndReturnNestedWithRequiredFieldAsList" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-nested-with-required-field-list" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "list", + "value": { + "type": "named", + "value": "type_types/object:NestedObjectWithRequiredField" + } + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithUnknownField": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithUnknownField", + "camelCase": { + "unsafeName": "getAndReturnWithUnknownField", + "safeName": "getAndReturnWithUnknownField" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_unknown_field", + "safeName": "get_and_return_with_unknown_field" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_UNKNOWN_FIELD", + "safeName": "GET_AND_RETURN_WITH_UNKNOWN_FIELD" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithUnknownField", + "safeName": "GetAndReturnWithUnknownField" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-unknown-field" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithUnknownField" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/object.getAndReturnWithDatetimeLikeString": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnWithDatetimeLikeString", + "camelCase": { + "unsafeName": "getAndReturnWithDatetimeLikeString", + "safeName": "getAndReturnWithDatetimeLikeString" + }, + "snakeCase": { + "unsafeName": "get_and_return_with_datetime_like_string", + "safeName": "get_and_return_with_datetime_like_string" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_WITH_DATETIME_LIKE_STRING", + "safeName": "GET_AND_RETURN_WITH_DATETIME_LIKE_STRING" + }, + "pascalCase": { + "unsafeName": "GetAndReturnWithDatetimeLikeString", + "safeName": "GetAndReturnWithDatetimeLikeString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + } + }, + "location": { + "method": "POST", + "path": "/object/get-and-return-with-datetime-like-string" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/object:ObjectWithDatetimeLikeString" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/pagination.listItems": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "listItems", + "camelCase": { + "unsafeName": "listItems", + "safeName": "listItems" + }, + "snakeCase": { + "unsafeName": "list_items", + "safeName": "list_items" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_ITEMS", + "safeName": "LIST_ITEMS" + }, + "pascalCase": { + "unsafeName": "ListItems", + "safeName": "ListItems" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + } + }, + "location": { + "method": "GET", + "path": "/pagination" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ListItemsRequest", + "camelCase": { + "unsafeName": "listItemsRequest", + "safeName": "listItemsRequest" + }, + "snakeCase": { + "unsafeName": "list_items_request", + "safeName": "list_items_request" + }, + "screamingSnakeCase": { + "unsafeName": "LIST_ITEMS_REQUEST", + "safeName": "LIST_ITEMS_REQUEST" + }, + "pascalCase": { + "unsafeName": "ListItemsRequest", + "safeName": "ListItemsRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "cursor", + "camelCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "snakeCase": { + "unsafeName": "cursor", + "safeName": "cursor" + }, + "screamingSnakeCase": { + "unsafeName": "CURSOR", + "safeName": "CURSOR" + }, + "pascalCase": { + "unsafeName": "Cursor", + "safeName": "Cursor" + } + }, + "wireValue": "cursor" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "limit", + "camelCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "snakeCase": { + "unsafeName": "limit", + "safeName": "limit" + }, + "screamingSnakeCase": { + "unsafeName": "LIMIT", + "safeName": "LIMIT" + }, + "pascalCase": { + "unsafeName": "Limit", + "safeName": "Limit" + } + }, + "wireValue": "limit" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithPath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithPath", + "camelCase": { + "unsafeName": "getWithPath", + "safeName": "getWithPath" + }, + "snakeCase": { + "unsafeName": "get_with_path", + "safeName": "get_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH", + "safeName": "GET_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithPath", + "safeName": "GetWithPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path/{param}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithInlinePath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithInlinePath", + "camelCase": { + "unsafeName": "getWithInlinePath", + "safeName": "getWithInlinePath" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path", + "safeName": "get_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH", + "safeName": "GET_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePath", + "safeName": "GetWithInlinePath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "getWithInlinePath", + "camelCase": { + "unsafeName": "getWithInlinePath", + "safeName": "getWithInlinePath" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path", + "safeName": "get_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH", + "safeName": "GET_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePath", + "safeName": "GetWithInlinePath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": true + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithQuery", + "camelCase": { + "unsafeName": "getWithQuery", + "safeName": "getWithQuery" + }, + "snakeCase": { + "unsafeName": "get_with_query", + "safeName": "get_with_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_QUERY", + "safeName": "GET_WITH_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithQuery", + "safeName": "GetWithQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetWithQuery", + "camelCase": { + "unsafeName": "getWithQuery", + "safeName": "getWithQuery" + }, + "snakeCase": { + "unsafeName": "get_with_query", + "safeName": "get_with_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_QUERY", + "safeName": "GET_WITH_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithQuery", + "safeName": "GetWithQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithAllowMultipleQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithAllowMultipleQuery", + "camelCase": { + "unsafeName": "getWithAllowMultipleQuery", + "safeName": "getWithAllowMultipleQuery" + }, + "snakeCase": { + "unsafeName": "get_with_allow_multiple_query", + "safeName": "get_with_allow_multiple_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_ALLOW_MULTIPLE_QUERY", + "safeName": "GET_WITH_ALLOW_MULTIPLE_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithAllowMultipleQuery", + "safeName": "GetWithAllowMultipleQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetWithMultipleQuery", + "camelCase": { + "unsafeName": "getWithMultipleQuery", + "safeName": "getWithMultipleQuery" + }, + "snakeCase": { + "unsafeName": "get_with_multiple_query", + "safeName": "get_with_multiple_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_MULTIPLE_QUERY", + "safeName": "GET_WITH_MULTIPLE_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithMultipleQuery", + "safeName": "GetWithMultipleQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "list", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "number", + "camelCase": { + "unsafeName": "number", + "safeName": "number" + }, + "snakeCase": { + "unsafeName": "number", + "safeName": "number" + }, + "screamingSnakeCase": { + "unsafeName": "NUMBER", + "safeName": "NUMBER" + }, + "pascalCase": { + "unsafeName": "Number", + "safeName": "Number" + } + }, + "wireValue": "number" + }, + "typeReference": { + "type": "list", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithPathAndQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithPathAndQuery", + "camelCase": { + "unsafeName": "getWithPathAndQuery", + "safeName": "getWithPathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_path_and_query", + "safeName": "get_with_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH_AND_QUERY", + "safeName": "GET_WITH_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithPathAndQuery", + "safeName": "GetWithPathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path-query/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "GetWithPathAndQuery", + "camelCase": { + "unsafeName": "getWithPathAndQuery", + "safeName": "getWithPathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_path_and_query", + "safeName": "get_with_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_PATH_AND_QUERY", + "safeName": "GET_WITH_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithPathAndQuery", + "safeName": "GetWithPathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.getWithInlinePathAndQuery": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithInlinePathAndQuery", + "camelCase": { + "unsafeName": "getWithInlinePathAndQuery", + "safeName": "getWithInlinePathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path_and_query", + "safeName": "get_with_inline_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH_AND_QUERY", + "safeName": "GET_WITH_INLINE_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePathAndQuery", + "safeName": "GetWithInlinePathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "GET", + "path": "/params/path-query/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "getWithInlinePathAndQuery", + "camelCase": { + "unsafeName": "getWithInlinePathAndQuery", + "safeName": "getWithInlinePathAndQuery" + }, + "snakeCase": { + "unsafeName": "get_with_inline_path_and_query", + "safeName": "get_with_inline_path_and_query" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_INLINE_PATH_AND_QUERY", + "safeName": "GET_WITH_INLINE_PATH_AND_QUERY" + }, + "pascalCase": { + "unsafeName": "GetWithInlinePathAndQuery", + "safeName": "GetWithInlinePathAndQuery" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [ + { + "name": { + "name": { + "originalName": "query", + "camelCase": { + "unsafeName": "query", + "safeName": "query" + }, + "snakeCase": { + "unsafeName": "query", + "safeName": "query" + }, + "screamingSnakeCase": { + "unsafeName": "QUERY", + "safeName": "QUERY" + }, + "pascalCase": { + "unsafeName": "Query", + "safeName": "Query" + } + }, + "wireValue": "query" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.modifyWithPath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "modifyWithPath", + "camelCase": { + "unsafeName": "modifyWithPath", + "safeName": "modifyWithPath" + }, + "snakeCase": { + "unsafeName": "modify_with_path", + "safeName": "modify_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_WITH_PATH", + "safeName": "MODIFY_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyWithPath", + "safeName": "ModifyWithPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/params/path/{param}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.modifyWithInlinePath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "modifyWithInlinePath", + "camelCase": { + "unsafeName": "modifyWithInlinePath", + "safeName": "modifyWithInlinePath" + }, + "snakeCase": { + "unsafeName": "modify_with_inline_path", + "safeName": "modify_with_inline_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_WITH_INLINE_PATH", + "safeName": "MODIFY_WITH_INLINE_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyWithInlinePath", + "safeName": "ModifyWithInlinePath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/params/path/{param}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ModifyResourceAtInlinedPath", + "camelCase": { + "unsafeName": "modifyResourceAtInlinedPath", + "safeName": "modifyResourceAtInlinedPath" + }, + "snakeCase": { + "unsafeName": "modify_resource_at_inlined_path", + "safeName": "modify_resource_at_inlined_path" + }, + "screamingSnakeCase": { + "unsafeName": "MODIFY_RESOURCE_AT_INLINED_PATH", + "safeName": "MODIFY_RESOURCE_AT_INLINED_PATH" + }, + "pascalCase": { + "unsafeName": "ModifyResourceAtInlinedPath", + "safeName": "ModifyResourceAtInlinedPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": { + "type": "referenced", + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "bodyType": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/params.uploadWithPath": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "uploadWithPath", + "camelCase": { + "unsafeName": "uploadWithPath", + "safeName": "uploadWithPath" + }, + "snakeCase": { + "unsafeName": "upload_with_path", + "safeName": "upload_with_path" + }, + "screamingSnakeCase": { + "unsafeName": "UPLOAD_WITH_PATH", + "safeName": "UPLOAD_WITH_PATH" + }, + "pascalCase": { + "unsafeName": "UploadWithPath", + "safeName": "UploadWithPath" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + } + }, + "location": { + "method": "POST", + "path": "/params/path/{param}" + }, + "request": { + "type": "body", + "pathParameters": [ + { + "name": { + "name": { + "originalName": "param", + "camelCase": { + "unsafeName": "param", + "safeName": "param" + }, + "snakeCase": { + "unsafeName": "param", + "safeName": "param" + }, + "screamingSnakeCase": { + "unsafeName": "PARAM", + "safeName": "PARAM" + }, + "pascalCase": { + "unsafeName": "Param", + "safeName": "Param" + } + }, + "wireValue": "param" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "bytes" + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnString": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnString", + "camelCase": { + "unsafeName": "getAndReturnString", + "safeName": "getAndReturnString" + }, + "snakeCase": { + "unsafeName": "get_and_return_string", + "safeName": "get_and_return_string" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_STRING", + "safeName": "GET_AND_RETURN_STRING" + }, + "pascalCase": { + "unsafeName": "GetAndReturnString", + "safeName": "GetAndReturnString" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/string" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnInt": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnInt", + "camelCase": { + "unsafeName": "getAndReturnInt", + "safeName": "getAndReturnInt" + }, + "snakeCase": { + "unsafeName": "get_and_return_int", + "safeName": "get_and_return_int" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_INT", + "safeName": "GET_AND_RETURN_INT" + }, + "pascalCase": { + "unsafeName": "GetAndReturnInt", + "safeName": "GetAndReturnInt" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/integer" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "INTEGER" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnLong": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnLong", + "camelCase": { + "unsafeName": "getAndReturnLong", + "safeName": "getAndReturnLong" + }, + "snakeCase": { + "unsafeName": "get_and_return_long", + "safeName": "get_and_return_long" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_LONG", + "safeName": "GET_AND_RETURN_LONG" + }, + "pascalCase": { + "unsafeName": "GetAndReturnLong", + "safeName": "GetAndReturnLong" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/long" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "LONG" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnDouble": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnDouble", + "camelCase": { + "unsafeName": "getAndReturnDouble", + "safeName": "getAndReturnDouble" + }, + "snakeCase": { + "unsafeName": "get_and_return_double", + "safeName": "get_and_return_double" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DOUBLE", + "safeName": "GET_AND_RETURN_DOUBLE" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDouble", + "safeName": "GetAndReturnDouble" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/double" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "DOUBLE" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnBool": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnBool", + "camelCase": { + "unsafeName": "getAndReturnBool", + "safeName": "getAndReturnBool" + }, + "snakeCase": { + "unsafeName": "get_and_return_bool", + "safeName": "get_and_return_bool" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_BOOL", + "safeName": "GET_AND_RETURN_BOOL" + }, + "pascalCase": { + "unsafeName": "GetAndReturnBool", + "safeName": "GetAndReturnBool" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/boolean" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "BOOLEAN" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnDatetime": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnDatetime", + "camelCase": { + "unsafeName": "getAndReturnDatetime", + "safeName": "getAndReturnDatetime" + }, + "snakeCase": { + "unsafeName": "get_and_return_datetime", + "safeName": "get_and_return_datetime" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DATETIME", + "safeName": "GET_AND_RETURN_DATETIME" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDatetime", + "safeName": "GetAndReturnDatetime" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/datetime" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "DATE_TIME" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnDate": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnDate", + "camelCase": { + "unsafeName": "getAndReturnDate", + "safeName": "getAndReturnDate" + }, + "snakeCase": { + "unsafeName": "get_and_return_date", + "safeName": "get_and_return_date" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_DATE", + "safeName": "GET_AND_RETURN_DATE" + }, + "pascalCase": { + "unsafeName": "GetAndReturnDate", + "safeName": "GetAndReturnDate" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/date" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "DATE" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnUUID": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnUUID", + "camelCase": { + "unsafeName": "getAndReturnUUID", + "safeName": "getAndReturnUUID" + }, + "snakeCase": { + "unsafeName": "get_and_return_uuid", + "safeName": "get_and_return_uuid" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_UUID", + "safeName": "GET_AND_RETURN_UUID" + }, + "pascalCase": { + "unsafeName": "GetAndReturnUUID", + "safeName": "GetAndReturnUUID" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/uuid" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "UUID" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/primitive.getAndReturnBase64": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnBase64", + "camelCase": { + "unsafeName": "getAndReturnBase64", + "safeName": "getAndReturnBase64" + }, + "snakeCase": { + "unsafeName": "get_and_return_base64", + "safeName": "get_and_return_base64" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_BASE64", + "safeName": "GET_AND_RETURN_BASE64" + }, + "pascalCase": { + "unsafeName": "GetAndReturnBase64", + "safeName": "GetAndReturnBase64" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + } + }, + "location": { + "method": "POST", + "path": "/primitive/base64" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "BASE_64" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/put.add": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "add", + "camelCase": { + "unsafeName": "add", + "safeName": "add" + }, + "snakeCase": { + "unsafeName": "add", + "safeName": "add" + }, + "screamingSnakeCase": { + "unsafeName": "ADD", + "safeName": "ADD" + }, + "pascalCase": { + "unsafeName": "Add", + "safeName": "Add" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "location": { + "method": "PUT", + "path": "/{id}" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "PutRequest", + "camelCase": { + "unsafeName": "putRequest", + "safeName": "putRequest" + }, + "snakeCase": { + "unsafeName": "put_request", + "safeName": "put_request" + }, + "screamingSnakeCase": { + "unsafeName": "PUT_REQUEST", + "safeName": "PUT_REQUEST" + }, + "pascalCase": { + "unsafeName": "PutRequest", + "safeName": "PutRequest" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + } + }, + "pathParameters": [ + { + "name": { + "name": { + "originalName": "id", + "camelCase": { + "unsafeName": "id", + "safeName": "id" + }, + "snakeCase": { + "unsafeName": "id", + "safeName": "id" + }, + "screamingSnakeCase": { + "unsafeName": "ID", + "safeName": "ID" + }, + "pascalCase": { + "unsafeName": "ID", + "safeName": "ID" + } + }, + "wireValue": "id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "queryParameters": [], + "headers": [], + "body": null, + "metadata": { + "includePathParameters": true, + "onlyPathParameters": true + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/union.getAndReturnUnion": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getAndReturnUnion", + "camelCase": { + "unsafeName": "getAndReturnUnion", + "safeName": "getAndReturnUnion" + }, + "snakeCase": { + "unsafeName": "get_and_return_union", + "safeName": "get_and_return_union" + }, + "screamingSnakeCase": { + "unsafeName": "GET_AND_RETURN_UNION", + "safeName": "GET_AND_RETURN_UNION" + }, + "pascalCase": { + "unsafeName": "GetAndReturnUnion", + "safeName": "GetAndReturnUnion" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + } + }, + "location": { + "method": "POST", + "path": "/union" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_types/union:Animal" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.withMixedCase": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "withMixedCase", + "camelCase": { + "unsafeName": "withMixedCase", + "safeName": "withMixedCase" + }, + "snakeCase": { + "unsafeName": "with_mixed_case", + "safeName": "with_mixed_case" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_MIXED_CASE", + "safeName": "WITH_MIXED_CASE" + }, + "pascalCase": { + "unsafeName": "WithMixedCase", + "safeName": "WithMixedCase" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/MixedCase" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.noEndingSlash": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "noEndingSlash", + "camelCase": { + "unsafeName": "noEndingSlash", + "safeName": "noEndingSlash" + }, + "snakeCase": { + "unsafeName": "no_ending_slash", + "safeName": "no_ending_slash" + }, + "screamingSnakeCase": { + "unsafeName": "NO_ENDING_SLASH", + "safeName": "NO_ENDING_SLASH" + }, + "pascalCase": { + "unsafeName": "NoEndingSlash", + "safeName": "NoEndingSlash" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/no-ending-slash" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.withEndingSlash": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "withEndingSlash", + "camelCase": { + "unsafeName": "withEndingSlash", + "safeName": "withEndingSlash" + }, + "snakeCase": { + "unsafeName": "with_ending_slash", + "safeName": "with_ending_slash" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_ENDING_SLASH", + "safeName": "WITH_ENDING_SLASH" + }, + "pascalCase": { + "unsafeName": "WithEndingSlash", + "safeName": "WithEndingSlash" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/with-ending-slash/" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_endpoints/urls.withUnderscores": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "withUnderscores", + "camelCase": { + "unsafeName": "withUnderscores", + "safeName": "withUnderscores" + }, + "snakeCase": { + "unsafeName": "with_underscores", + "safeName": "with_underscores" + }, + "screamingSnakeCase": { + "unsafeName": "WITH_UNDERSCORES", + "safeName": "WITH_UNDERSCORES" + }, + "pascalCase": { + "unsafeName": "WithUnderscores", + "safeName": "WithUnderscores" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + } + }, + "location": { + "method": "GET", + "path": "/urls/with_underscores" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_inlined-requests.postWithObjectBodyandResponse": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postWithObjectBodyandResponse", + "camelCase": { + "unsafeName": "postWithObjectBodyandResponse", + "safeName": "postWithObjectBodyandResponse" + }, + "snakeCase": { + "unsafeName": "post_with_object_bodyand_response", + "safeName": "post_with_object_bodyand_response" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_OBJECT_BODYAND_RESPONSE", + "safeName": "POST_WITH_OBJECT_BODYAND_RESPONSE" + }, + "pascalCase": { + "unsafeName": "PostWithObjectBodyandResponse", + "safeName": "PostWithObjectBodyandResponse" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + ], + "packagePath": [], + "file": { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + } + }, + "location": { + "method": "POST", + "path": "/req-bodies/object" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "PostWithObjectBody", + "camelCase": { + "unsafeName": "postWithObjectBody", + "safeName": "postWithObjectBody" + }, + "snakeCase": { + "unsafeName": "post_with_object_body", + "safeName": "post_with_object_body" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_OBJECT_BODY", + "safeName": "POST_WITH_OBJECT_BODY" + }, + "pascalCase": { + "unsafeName": "PostWithObjectBody", + "safeName": "PostWithObjectBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + ], + "packagePath": [], + "file": { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [], + "body": { + "type": "properties", + "value": [ + { + "name": { + "name": { + "originalName": "string", + "camelCase": { + "unsafeName": "string", + "safeName": "string" + }, + "snakeCase": { + "unsafeName": "string", + "safeName": "string" + }, + "screamingSnakeCase": { + "unsafeName": "STRING", + "safeName": "STRING" + }, + "pascalCase": { + "unsafeName": "String", + "safeName": "String" + } + }, + "wireValue": "string" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "integer", + "camelCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "snakeCase": { + "unsafeName": "integer", + "safeName": "integer" + }, + "screamingSnakeCase": { + "unsafeName": "INTEGER", + "safeName": "INTEGER" + }, + "pascalCase": { + "unsafeName": "Integer", + "safeName": "Integer" + } + }, + "wireValue": "integer" + }, + "typeReference": { + "type": "primitive", + "value": "INTEGER" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "NestedObject", + "camelCase": { + "unsafeName": "nestedObject", + "safeName": "nestedObject" + }, + "snakeCase": { + "unsafeName": "nested_object", + "safeName": "nested_object" + }, + "screamingSnakeCase": { + "unsafeName": "NESTED_OBJECT", + "safeName": "NESTED_OBJECT" + }, + "pascalCase": { + "unsafeName": "NestedObject", + "safeName": "NestedObject" + } + }, + "wireValue": "NestedObject" + }, + "typeReference": { + "type": "named", + "value": "type_types/object:ObjectWithOptionalField" + }, + "propertyAccess": null, + "variable": null + } + ] + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_no-auth.postWithNoAuth": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postWithNoAuth", + "camelCase": { + "unsafeName": "postWithNoAuth", + "safeName": "postWithNoAuth" + }, + "snakeCase": { + "unsafeName": "post_with_no_auth", + "safeName": "post_with_no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_NO_AUTH", + "safeName": "POST_WITH_NO_AUTH" + }, + "pascalCase": { + "unsafeName": "PostWithNoAuth", + "safeName": "PostWithNoAuth" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + } + }, + "location": { + "method": "POST", + "path": "/no-auth" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "unknown" + } + } + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_no-req-body.getWithNoRequestBody": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithNoRequestBody", + "camelCase": { + "unsafeName": "getWithNoRequestBody", + "safeName": "getWithNoRequestBody" + }, + "snakeCase": { + "unsafeName": "get_with_no_request_body", + "safeName": "get_with_no_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_NO_REQUEST_BODY", + "safeName": "GET_WITH_NO_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "GetWithNoRequestBody", + "safeName": "GetWithNoRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + } + }, + "location": { + "method": "GET", + "path": "/no-req-body" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_no-req-body.postWithNoRequestBody": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "postWithNoRequestBody", + "camelCase": { + "unsafeName": "postWithNoRequestBody", + "safeName": "postWithNoRequestBody" + }, + "snakeCase": { + "unsafeName": "post_with_no_request_body", + "safeName": "post_with_no_request_body" + }, + "screamingSnakeCase": { + "unsafeName": "POST_WITH_NO_REQUEST_BODY", + "safeName": "POST_WITH_NO_REQUEST_BODY" + }, + "pascalCase": { + "unsafeName": "PostWithNoRequestBody", + "safeName": "PostWithNoRequestBody" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + } + }, + "location": { + "method": "POST", + "path": "/no-req-body" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": null + }, + "response": { + "type": "json" + }, + "examples": null + }, + "endpoint_req-with-headers.getWithCustomHeader": { + "auth": { + "type": "bearer", + "token": { + "originalName": "token", + "camelCase": { + "unsafeName": "token", + "safeName": "token" + }, + "snakeCase": { + "unsafeName": "token", + "safeName": "token" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN", + "safeName": "TOKEN" + }, + "pascalCase": { + "unsafeName": "Token", + "safeName": "Token" + } + } + }, + "declaration": { + "name": { + "originalName": "getWithCustomHeader", + "camelCase": { + "unsafeName": "getWithCustomHeader", + "safeName": "getWithCustomHeader" + }, + "snakeCase": { + "unsafeName": "get_with_custom_header", + "safeName": "get_with_custom_header" + }, + "screamingSnakeCase": { + "unsafeName": "GET_WITH_CUSTOM_HEADER", + "safeName": "GET_WITH_CUSTOM_HEADER" + }, + "pascalCase": { + "unsafeName": "GetWithCustomHeader", + "safeName": "GetWithCustomHeader" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + ], + "packagePath": [], + "file": { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + } + }, + "location": { + "method": "POST", + "path": "/test-headers/custom-header" + }, + "request": { + "type": "inlined", + "declaration": { + "name": { + "originalName": "ReqWithHeaders", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + }, + "fernFilepath": { + "allParts": [ + { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + ], + "packagePath": [], + "file": { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + } + }, + "pathParameters": [], + "queryParameters": [], + "headers": [ + { + "name": { + "name": { + "originalName": "X-TEST-SERVICE-HEADER", + "camelCase": { + "unsafeName": "xTestServiceHeader", + "safeName": "xTestServiceHeader" + }, + "snakeCase": { + "unsafeName": "x_test_service_header", + "safeName": "x_test_service_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_SERVICE_HEADER", + "safeName": "X_TEST_SERVICE_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestServiceHeader", + "safeName": "XTestServiceHeader" + } + }, + "wireValue": "X-TEST-SERVICE-HEADER" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "X-TEST-ENDPOINT-HEADER", + "camelCase": { + "unsafeName": "xTestEndpointHeader", + "safeName": "xTestEndpointHeader" + }, + "snakeCase": { + "unsafeName": "x_test_endpoint_header", + "safeName": "x_test_endpoint_header" + }, + "screamingSnakeCase": { + "unsafeName": "X_TEST_ENDPOINT_HEADER", + "safeName": "X_TEST_ENDPOINT_HEADER" + }, + "pascalCase": { + "unsafeName": "XTestEndpointHeader", + "safeName": "XTestEndpointHeader" + } + }, + "wireValue": "X-TEST-ENDPOINT-HEADER" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "body": { + "type": "referenced", + "bodyKey": { + "originalName": "body", + "camelCase": { + "unsafeName": "body", + "safeName": "body" + }, + "snakeCase": { + "unsafeName": "body", + "safeName": "body" + }, + "screamingSnakeCase": { + "unsafeName": "BODY", + "safeName": "BODY" + }, + "pascalCase": { + "unsafeName": "Body", + "safeName": "Body" + } + }, + "bodyType": { + "type": "typeReference", + "value": { + "type": "primitive", + "value": "STRING" + } + } + }, + "metadata": { + "includePathParameters": false, + "onlyPathParameters": false + } + }, + "response": { + "type": "json" + }, + "examples": null + } + }, + "pathParameters": [], + "environments": null, + "variables": null, + "generatorConfig": null + }, + "audiences": null, + "generationMetadata": null, + "apiPlayground": true, + "subpackages": { + "subpackage_endpoints": { + "name": { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": null + }, + "service": null, + "types": [], + "errors": [], + "subpackages": [ + "subpackage_endpoints/container", + "subpackage_endpoints/content-type", + "subpackage_endpoints/duplicate-names-a", + "subpackage_endpoints/duplicate-names-b", + "subpackage_endpoints/duplicate-names-c", + "subpackage_endpoints/enum", + "subpackage_endpoints/http-methods", + "subpackage_endpoints/object", + "subpackage_endpoints/pagination", + "subpackage_endpoints/params", + "subpackage_endpoints/primitive", + "subpackage_endpoints/put", + "subpackage_endpoints/union", + "subpackage_endpoints/urls" + ], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/container": { + "name": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "container", + "camelCase": { + "unsafeName": "container", + "safeName": "container" + }, + "snakeCase": { + "unsafeName": "container", + "safeName": "container" + }, + "screamingSnakeCase": { + "unsafeName": "CONTAINER", + "safeName": "CONTAINER" + }, + "pascalCase": { + "unsafeName": "Container", + "safeName": "Container" + } + } + }, + "service": "service_endpoints/container", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/content-type": { + "name": { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "content-type", + "camelCase": { + "unsafeName": "contentType", + "safeName": "contentType" + }, + "snakeCase": { + "unsafeName": "content_type", + "safeName": "content_type" + }, + "screamingSnakeCase": { + "unsafeName": "CONTENT_TYPE", + "safeName": "CONTENT_TYPE" + }, + "pascalCase": { + "unsafeName": "ContentType", + "safeName": "ContentType" + } + } + }, + "service": "service_endpoints/content-type", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/duplicate-names-a": { + "name": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-a", + "camelCase": { + "unsafeName": "duplicateNamesA", + "safeName": "duplicateNamesA" + }, + "snakeCase": { + "unsafeName": "duplicate_names_a", + "safeName": "duplicate_names_a" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_A", + "safeName": "DUPLICATE_NAMES_A" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesA", + "safeName": "DuplicateNamesA" + } + } + }, + "service": "service_endpoints/duplicate-names-a", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/duplicate-names-b": { + "name": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-b", + "camelCase": { + "unsafeName": "duplicateNamesB", + "safeName": "duplicateNamesB" + }, + "snakeCase": { + "unsafeName": "duplicate_names_b", + "safeName": "duplicate_names_b" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_B", + "safeName": "DUPLICATE_NAMES_B" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesB", + "safeName": "DuplicateNamesB" + } + } + }, + "service": "service_endpoints/duplicate-names-b", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/duplicate-names-c": { + "name": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "duplicate-names-c", + "camelCase": { + "unsafeName": "duplicateNamesC", + "safeName": "duplicateNamesC" + }, + "snakeCase": { + "unsafeName": "duplicate_names_c", + "safeName": "duplicate_names_c" + }, + "screamingSnakeCase": { + "unsafeName": "DUPLICATE_NAMES_C", + "safeName": "DUPLICATE_NAMES_C" + }, + "pascalCase": { + "unsafeName": "DuplicateNamesC", + "safeName": "DuplicateNamesC" + } + } + }, + "service": "service_endpoints/duplicate-names-c", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/enum": { + "name": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "service": "service_endpoints/enum", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/http-methods": { + "name": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "http-methods", + "camelCase": { + "unsafeName": "httpMethods", + "safeName": "httpMethods" + }, + "snakeCase": { + "unsafeName": "http_methods", + "safeName": "http_methods" + }, + "screamingSnakeCase": { + "unsafeName": "HTTP_METHODS", + "safeName": "HTTP_METHODS" + }, + "pascalCase": { + "unsafeName": "HTTPMethods", + "safeName": "HTTPMethods" + } + } + }, + "service": "service_endpoints/http-methods", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/object": { + "name": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "service": "service_endpoints/object", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/pagination": { + "name": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "pagination", + "camelCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "snakeCase": { + "unsafeName": "pagination", + "safeName": "pagination" + }, + "screamingSnakeCase": { + "unsafeName": "PAGINATION", + "safeName": "PAGINATION" + }, + "pascalCase": { + "unsafeName": "Pagination", + "safeName": "Pagination" + } + } + }, + "service": "service_endpoints/pagination", + "types": [ + "type_endpoints/pagination:PaginatedResponse" + ], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/params": { + "name": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "params", + "camelCase": { + "unsafeName": "params", + "safeName": "params" + }, + "snakeCase": { + "unsafeName": "params", + "safeName": "params" + }, + "screamingSnakeCase": { + "unsafeName": "PARAMS", + "safeName": "PARAMS" + }, + "pascalCase": { + "unsafeName": "Params", + "safeName": "Params" + } + } + }, + "service": "service_endpoints/params", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/primitive": { + "name": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "primitive", + "camelCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "snakeCase": { + "unsafeName": "primitive", + "safeName": "primitive" + }, + "screamingSnakeCase": { + "unsafeName": "PRIMITIVE", + "safeName": "PRIMITIVE" + }, + "pascalCase": { + "unsafeName": "Primitive", + "safeName": "Primitive" + } + } + }, + "service": "service_endpoints/primitive", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/put": { + "name": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "put", + "camelCase": { + "unsafeName": "put", + "safeName": "put" + }, + "snakeCase": { + "unsafeName": "put", + "safeName": "put" + }, + "screamingSnakeCase": { + "unsafeName": "PUT", + "safeName": "PUT" + }, + "pascalCase": { + "unsafeName": "Put", + "safeName": "Put" + } + } + }, + "service": "service_endpoints/put", + "types": [ + "type_endpoints/put:Error", + "type_endpoints/put:ErrorCategory", + "type_endpoints/put:ErrorCode", + "type_endpoints/put:PutResponse" + ], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/union": { + "name": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "service": "service_endpoints/union", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_endpoints/urls": { + "name": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + }, + { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + ], + "packagePath": [ + { + "originalName": "endpoints", + "camelCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "snakeCase": { + "unsafeName": "endpoints", + "safeName": "endpoints" + }, + "screamingSnakeCase": { + "unsafeName": "ENDPOINTS", + "safeName": "ENDPOINTS" + }, + "pascalCase": { + "unsafeName": "Endpoints", + "safeName": "Endpoints" + } + } + ], + "file": { + "originalName": "urls", + "camelCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "snakeCase": { + "unsafeName": "urls", + "safeName": "urls" + }, + "screamingSnakeCase": { + "unsafeName": "URLS", + "safeName": "URLS" + }, + "pascalCase": { + "unsafeName": "URLs", + "safeName": "URLs" + } + } + }, + "service": "service_endpoints/urls", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_general-errors": { + "name": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + ], + "packagePath": [], + "file": { + "originalName": "general-errors", + "camelCase": { + "unsafeName": "generalErrors", + "safeName": "generalErrors" + }, + "snakeCase": { + "unsafeName": "general_errors", + "safeName": "general_errors" + }, + "screamingSnakeCase": { + "unsafeName": "GENERAL_ERRORS", + "safeName": "GENERAL_ERRORS" + }, + "pascalCase": { + "unsafeName": "GeneralErrors", + "safeName": "GeneralErrors" + } + } + }, + "service": null, + "types": [ + "type_general-errors:BadObjectRequestInfo" + ], + "errors": [ + "error_general-errors:BadRequestBody" + ], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": false, + "docs": null + }, + "subpackage_inlined-requests": { + "name": { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + ], + "packagePath": [], + "file": { + "originalName": "inlined-requests", + "camelCase": { + "unsafeName": "inlinedRequests", + "safeName": "inlinedRequests" + }, + "snakeCase": { + "unsafeName": "inlined_requests", + "safeName": "inlined_requests" + }, + "screamingSnakeCase": { + "unsafeName": "INLINED_REQUESTS", + "safeName": "INLINED_REQUESTS" + }, + "pascalCase": { + "unsafeName": "InlinedRequests", + "safeName": "InlinedRequests" + } + } + }, + "service": "service_inlined-requests", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_no-auth": { + "name": { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-auth", + "camelCase": { + "unsafeName": "noAuth", + "safeName": "noAuth" + }, + "snakeCase": { + "unsafeName": "no_auth", + "safeName": "no_auth" + }, + "screamingSnakeCase": { + "unsafeName": "NO_AUTH", + "safeName": "NO_AUTH" + }, + "pascalCase": { + "unsafeName": "NoAuth", + "safeName": "NoAuth" + } + } + }, + "service": "service_no-auth", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_no-req-body": { + "name": { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + ], + "packagePath": [], + "file": { + "originalName": "no-req-body", + "camelCase": { + "unsafeName": "noReqBody", + "safeName": "noReqBody" + }, + "snakeCase": { + "unsafeName": "no_req_body", + "safeName": "no_req_body" + }, + "screamingSnakeCase": { + "unsafeName": "NO_REQ_BODY", + "safeName": "NO_REQ_BODY" + }, + "pascalCase": { + "unsafeName": "NoReqBody", + "safeName": "NoReqBody" + } + } + }, + "service": "service_no-req-body", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_req-with-headers": { + "name": { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + ], + "packagePath": [], + "file": { + "originalName": "req-with-headers", + "camelCase": { + "unsafeName": "reqWithHeaders", + "safeName": "reqWithHeaders" + }, + "snakeCase": { + "unsafeName": "req_with_headers", + "safeName": "req_with_headers" + }, + "screamingSnakeCase": { + "unsafeName": "REQ_WITH_HEADERS", + "safeName": "REQ_WITH_HEADERS" + }, + "pascalCase": { + "unsafeName": "ReqWithHeaders", + "safeName": "ReqWithHeaders" + } + } + }, + "service": "service_req-with-headers", + "types": [], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": true, + "docs": null + }, + "subpackage_types": { + "name": { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": null + }, + "service": null, + "types": [], + "errors": [], + "subpackages": [ + "subpackage_types/docs", + "subpackage_types/enum", + "subpackage_types/object", + "subpackage_types/union" + ], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": false, + "docs": null + }, + "subpackage_types/docs": { + "name": { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "docs", + "camelCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "snakeCase": { + "unsafeName": "docs", + "safeName": "docs" + }, + "screamingSnakeCase": { + "unsafeName": "DOCS", + "safeName": "DOCS" + }, + "pascalCase": { + "unsafeName": "Docs", + "safeName": "Docs" + } + } + }, + "service": null, + "types": [ + "type_types/docs:ObjectWithDocs" + ], + "errors": [], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": false, + "docs": null + }, + "subpackage_types/enum": { + "name": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "enum", + "camelCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "snakeCase": { + "unsafeName": "enum", + "safeName": "enum" + }, + "screamingSnakeCase": { + "unsafeName": "ENUM", + "safeName": "ENUM" + }, + "pascalCase": { + "unsafeName": "Enum", + "safeName": "Enum" + } + } + }, + "service": null, + "types": [ + "type_types/enum:WeatherReport" + ], + "errors": [ + "error_types/enum:ErrorWithEnumBody" + ], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": false, + "docs": null + }, + "subpackage_types/object": { + "name": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "object", + "camelCase": { + "unsafeName": "object", + "safeName": "object" + }, + "snakeCase": { + "unsafeName": "object", + "safeName": "object" + }, + "screamingSnakeCase": { + "unsafeName": "OBJECT", + "safeName": "OBJECT" + }, + "pascalCase": { + "unsafeName": "Object", + "safeName": "Object" + } + } + }, + "service": null, + "types": [ + "type_types/object:ObjectWithOptionalField", + "type_types/object:ObjectWithRequiredField", + "type_types/object:ObjectWithMapOfMap", + "type_types/object:NestedObjectWithOptionalField", + "type_types/object:NestedObjectWithRequiredField", + "type_types/object:DoubleOptional", + "type_types/object:OptionalAlias", + "type_types/object:ObjectWithDatetimeLikeString", + "type_types/object:ObjectWithUnknownField" + ], + "errors": [ + "error_types/object:ObjectWithOptionalFieldError", + "error_types/object:ObjectWithRequiredFieldError", + "error_types/object:NestedObjectWithOptionalFieldError", + "error_types/object:NestedObjectWithRequiredFieldError" + ], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": false, + "docs": null + }, + "subpackage_types/union": { + "name": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + }, + "displayName": null, + "fernFilepath": { + "allParts": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + }, + { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + ], + "packagePath": [ + { + "originalName": "types", + "camelCase": { + "unsafeName": "types", + "safeName": "types" + }, + "snakeCase": { + "unsafeName": "types", + "safeName": "types" + }, + "screamingSnakeCase": { + "unsafeName": "TYPES", + "safeName": "TYPES" + }, + "pascalCase": { + "unsafeName": "Types", + "safeName": "Types" + } + } + ], + "file": { + "originalName": "union", + "camelCase": { + "unsafeName": "union", + "safeName": "union" + }, + "snakeCase": { + "unsafeName": "union", + "safeName": "union" + }, + "screamingSnakeCase": { + "unsafeName": "UNION", + "safeName": "UNION" + }, + "pascalCase": { + "unsafeName": "Union", + "safeName": "Union" + } + } + }, + "service": null, + "types": [ + "type_types/union:Animal", + "type_types/union:Dog", + "type_types/union:Cat", + "type_types/union:MixedType" + ], + "errors": [ + "error_types/union:ErrorWithUnionBody" + ], + "subpackages": [], + "navigationConfig": null, + "webhooks": null, + "websocket": null, + "hasEndpointsInTree": false, + "docs": null + } + }, + "rootPackage": { + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "websocket": null, + "service": null, + "types": [], + "errors": [], + "subpackages": [ + "subpackage_endpoints", + "subpackage_general-errors", + "subpackage_inlined-requests", + "subpackage_no-auth", + "subpackage_no-req-body", + "subpackage_req-with-headers", + "subpackage_types" + ], + "webhooks": null, + "navigationConfig": null, + "hasEndpointsInTree": true, + "docs": null + }, + "sdkConfig": { + "isAuthMandatory": false, + "hasStreamingEndpoints": false, + "hasPaginatedEndpoints": true, + "hasFileDownloadEndpoints": false, + "platformHeaders": { + "language": "X-Fern-Language", + "sdkName": "X-Fern-SDK-Name", + "sdkVersion": "X-Fern-SDK-Version", + "userAgent": null + } + } +} \ No newline at end of file diff --git a/seed/go-sdk/go-deterministic-ordering/.fern/metadata.json b/seed/go-sdk/go-deterministic-ordering/.fern/metadata.json new file mode 100644 index 000000000000..f539e2d7bcf6 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/.fern/metadata.json @@ -0,0 +1,11 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-go-sdk", + "generatorVersion": "latest", + "generatorConfig": { + "enableWireTests": true, + "exportAllRequestsAtRoot": true + }, + "originGitCommit": "DUMMY", + "sdkVersion": "v0.0.1" +} \ No newline at end of file diff --git a/seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml b/seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml new file mode 100644 index 000000000000..588cbe574004 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Compile + run: go build ./... + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.10.1 + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Setup wiremock server + run: | + PROJECT_NAME="wiremock-$(basename $(dirname $(pwd)) | tr -d '.')" + echo "PROJECT_NAME=$PROJECT_NAME" >> $GITHUB_ENV + if [ -f wiremock/docker-compose.test.yml ]; then + docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml down + docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml up -d + WIREMOCK_PORT=$(docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml port wiremock 8080 | cut -d: -f2) + echo "WIREMOCK_URL=http://localhost:$WIREMOCK_PORT" >> $GITHUB_ENV + fi + + - name: Test + run: go test ./... + + - name: Teardown wiremock server + if: always() + run: | + if [ -f wiremock/docker-compose.test.yml ]; then + docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml down + fi diff --git a/seed/go-sdk/go-deterministic-ordering/README.md b/seed/go-sdk/go-deterministic-ordering/README.md new file mode 100644 index 000000000000..d5c9aa1f889b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/README.md @@ -0,0 +1,198 @@ +# Seed Go Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FGo) + +The Seed Go library provides convenient access to the Seed APIs from Go. + +## Table of Contents + +- [Reference](#reference) +- [Usage](#usage) +- [Environments](#environments) +- [Errors](#errors) +- [Request Options](#request-options) +- [Advanced](#advanced) + - [Response Headers](#response-headers) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Explicit Null](#explicit-null) +- [Contributing](#contributing) + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```go +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithToken( + "", + ), + ) + request := []string{ + "string", + "string", + } + client.Endpoints.Container.GetAndReturnListOfPrimitives( + context.TODO(), + request, + ) +} +``` + +## Environments + +You can choose between different environments by using the `option.WithBaseURL` option. You can configure any arbitrary base +URL, which is particularly useful in test environments. + +```go +client := client.NewClient( + option.WithBaseURL("https://example.com"), +) +``` + +## Errors + +Structured error types are returned from API calls that return non-success status codes. These errors are compatible +with the `errors.Is` and `errors.As` APIs, so you can access the error like so: + +```go +response, err := client.Endpoints.Container.GetAndReturnListOfPrimitives(...) +if err != nil { + var apiError *core.APIError + if errors.As(err, apiError) { + // Do something with the API error ... + } + return err +} +``` + +## Request Options + +A variety of request options are included to adapt the behavior of the library, which includes configuring +authorization tokens, or providing your own instrumented `*http.Client`. + +These request options can either be +specified on the client so that they're applied on every request, or for an individual request, like so: + +> Providing your own `*http.Client` is recommended. Otherwise, the `http.DefaultClient` will be used, +> and your client will wait indefinitely for a response (unless the per-request, context-based timeout +> is used). + +```go +// Specify default options applied on every request. +client := client.NewClient( + option.WithToken(""), + option.WithHTTPClient( + &http.Client{ + Timeout: 5 * time.Second, + }, + ), +) + +// Specify options for an individual request. +response, err := client.Endpoints.Container.GetAndReturnListOfPrimitives( + ..., + option.WithToken(""), +) +``` + +## Advanced + +### Response Headers + +You can access the raw HTTP response data by using the `WithRawResponse` field on the client. This is useful +when you need to examine the response headers received from the API call. (When the endpoint is paginated, +the raw HTTP response data will be included automatically in the Page response object.) + +```go +response, err := client.Endpoints.Container.WithRawResponse.GetAndReturnListOfPrimitives(...) +if err != nil { + return err +} +fmt.Printf("Got response headers: %v", response.Header) +fmt.Printf("Got status code: %d", response.StatusCode) +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +If the `Retry-After` header is present in the response, the SDK will prioritize respecting its value exactly +over the default exponential backoff. + +Use the `option.WithMaxAttempts` option to configure this behavior for the entire client or an individual request: + +```go +client := client.NewClient( + option.WithMaxAttempts(1), +) + +response, err := client.Endpoints.Container.GetAndReturnListOfPrimitives( + ..., + option.WithMaxAttempts(1), +) +``` + +### Timeouts + +Setting a timeout for each individual request is as simple as using the standard context library. Setting a one second timeout for an individual API call looks like the following: + +```go +ctx, cancel := context.WithTimeout(ctx, time.Second) +defer cancel() + +response, err := client.Endpoints.Container.GetAndReturnListOfPrimitives(ctx, ...) +``` + +### Explicit Null + +If you want to send the explicit `null` JSON value through an optional parameter, you can use the setters\ +that come with every object. Calling a setter method for a property will flip a bit in the `explicitFields` +bitfield for that setter's object; during serialization, any property with a flipped bit will have its +omittable status stripped, so zero or `nil` values will be sent explicitly rather than omitted altogether: + +```go +type ExampleRequest struct { + // An optional string parameter. + Name *string `json:"name,omitempty" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +request := &ExampleRequest{} +request.SetName(nil) + +response, err := client.Endpoints.Container.GetAndReturnListOfPrimitives(ctx, request, ...) +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/go-sdk/go-deterministic-ordering/client/client.go b/seed/go-sdk/go-deterministic-ordering/client/client.go new file mode 100644 index 000000000000..add41880a0e4 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/client/client.go @@ -0,0 +1,45 @@ +// Code generated by Fern. DO NOT EDIT. + +package client + +import ( + core "github.com/go-deterministic-ordering/fern/core" + client "github.com/go-deterministic-ordering/fern/endpoints/client" + inlinedrequests "github.com/go-deterministic-ordering/fern/inlinedrequests" + internal "github.com/go-deterministic-ordering/fern/internal" + noauth "github.com/go-deterministic-ordering/fern/noauth" + noreqbody "github.com/go-deterministic-ordering/fern/noreqbody" + option "github.com/go-deterministic-ordering/fern/option" + reqwithheaders "github.com/go-deterministic-ordering/fern/reqwithheaders" +) + +type Client struct { + Endpoints *client.Client + InlinedRequests *inlinedrequests.Client + NoAuth *noauth.Client + NoReqBody *noreqbody.Client + ReqWithHeaders *reqwithheaders.Client + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + Endpoints: client.NewClient(options), + InlinedRequests: inlinedrequests.NewClient(options), + NoAuth: noauth.NewClient(options), + NoReqBody: noreqbody.NewClient(options), + ReqWithHeaders: reqwithheaders.NewClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/client/client_test.go b/seed/go-sdk/go-deterministic-ordering/client/client_test.go new file mode 100644 index 000000000000..7323dd145fa7 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/client/client_test.go @@ -0,0 +1,45 @@ +// Code generated by Fern. DO NOT EDIT. + +package client + +import ( + option "github.com/go-deterministic-ordering/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.options.HTTPHeader.Get("X-API-Tenancy")) + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/core/api_error.go b/seed/go-sdk/go-deterministic-ordering/core/api_error.go new file mode 100644 index 000000000000..6168388541b4 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/core/api_error.go @@ -0,0 +1,47 @@ +package core + +import ( + "fmt" + "net/http" +) + +// 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:"-"` + Header http.Header `json:"-"` +} + +// NewAPIError constructs a new API error. +func NewAPIError(statusCode int, header http.Header, err error) *APIError { + return &APIError{ + err: err, + Header: header, + StatusCode: statusCode, + } +} + +// Unwrap returns the underlying error. This also makes the error compatible +// with errors.As and errors.Is. +func (a *APIError) Unwrap() error { + if a == nil { + return nil + } + return a.err +} + +// Error returns the API error's message. +func (a *APIError) Error() string { + if a == nil || (a.err == nil && a.StatusCode == 0) { + return "" + } + if a.err == nil { + return fmt.Sprintf("%d", a.StatusCode) + } + if a.StatusCode == 0 { + return a.err.Error() + } + return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) +} diff --git a/seed/go-sdk/go-deterministic-ordering/core/http.go b/seed/go-sdk/go-deterministic-ordering/core/http.go new file mode 100644 index 000000000000..92c435692940 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/core/http.go @@ -0,0 +1,15 @@ +package core + +import "net/http" + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// Response is an HTTP response from an HTTP client. +type Response[T any] struct { + StatusCode int + Header http.Header + Body T +} diff --git a/seed/go-sdk/go-deterministic-ordering/core/page.go b/seed/go-sdk/go-deterministic-ordering/core/page.go new file mode 100644 index 000000000000..98c5b7bbc0b2 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/core/page.go @@ -0,0 +1,96 @@ +package core + +import ( + "context" + "errors" + "net/http" +) + +// ErrNoPages is a sentinel error used to signal that no pages remain. +// +// This error should be used similar to io.EOF, such that it represents +// a non-actionable error. +var ErrNoPages = errors.New("no pages remain") + +// PageRequest represents the information required to identify a single page. +type PageRequest[Cursor comparable] struct { + Cursor Cursor + + // Holds the value of the response type (populated by the *Caller). + Response any +} + +// PageResponse represents the information associated with a single page. +// +// Type parameters: +// - Cursor: the type used for pagination (e.g., string cursor or integer offset) +// - T: the type of individual items in the page +// - R: the response type returned by the paginated endpoint +type PageResponse[Cursor comparable, T any, R any] struct { + Results []T + Response R + Next Cursor + Done bool +} + +// Page represents a single page of results. +type Page[Cursor comparable, T any, R any] struct { + Results []T + Response R + RawResponse PageResponse[Cursor, T, R] + StatusCode int + Header http.Header + NextPageFunc func(context.Context) (*Page[Cursor, T, R], error) +} + +// GetNextPage fetches the next page, if any. If no pages remain, +// the ErrNoPages error is returned. +func (p *Page[Cursor, T, R]) GetNextPage(ctx context.Context) (*Page[Cursor, T, R], error) { + return p.NextPageFunc(ctx) +} + +// Iterator returns an iterator that starts at the current page. +func (p *Page[Cursor, T, R]) Iterator() *PageIterator[Cursor, T, R] { + return &PageIterator[Cursor, T, R]{ + page: p, + } +} + +// PageIterator is an auto-iterator for paginated endpoints. +type PageIterator[Cursor comparable, T any, R any] struct { + page *Page[Cursor, T, R] + current T + index int + err error +} + +// Next returns true if the given iterator has more results, +// fetching the next page as needed. +func (p *PageIterator[Cursor, T, R]) Next(ctx context.Context) bool { + if p.page == nil || len(p.page.Results) == 0 { + return false + } + if p.index >= len(p.page.Results) { + p.index = 0 + p.page, p.err = p.page.GetNextPage(ctx) + if p.err != nil || p.page == nil || len(p.page.Results) == 0 { + return false + } + } + p.current = p.page.Results[p.index] + p.index += 1 + return true +} + +// Current returns the current element. +func (p *PageIterator[Cursor, T, R]) Current() T { + return p.current +} + +// Err returns a non-nil error if the iterator encountered an error. +func (p *PageIterator[Cursor, T, R]) Err() error { + if errors.Is(p.err, ErrNoPages) { + return nil + } + return p.err +} diff --git a/seed/go-sdk/go-deterministic-ordering/core/request_option.go b/seed/go-sdk/go-deterministic-ordering/core/request_option.go new file mode 100644 index 000000000000..ef888afa6d45 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/core/request_option.go @@ -0,0 +1,135 @@ +// Code generated by Fern. DO NOT EDIT. + +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 + MaxBufSize int + 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/go-deterministic-ordering/fern") + headers.Set("X-Fern-SDK-Version", "v0.0.1") + headers.Set("User-Agent", "github.com/go-deterministic-ordering/fern/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 +} + +// MaxBufSizeOption implements the RequestOption interface. +type MaxBufSizeOption struct { + MaxBufSize int +} + +func (m *MaxBufSizeOption) applyRequestOptions(opts *RequestOptions) { + opts.MaxBufSize = m.MaxBufSize +} + +// 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/go-deterministic-ordering/dynamic-snippets/example0/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example0/snippet.go new file mode 100644 index 000000000000..bc7c13dea4cf --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example0/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := []string{ + "string", + "string", + } + client.Endpoints.Container.GetAndReturnListOfPrimitives( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example1/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example1/snippet.go new file mode 100644 index 000000000000..760e33186f7c --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example1/snippet.go @@ -0,0 +1,31 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := []*types.ObjectWithRequiredField{ + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } + client.Endpoints.Container.GetAndReturnListOfObjects( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example10/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example10/snippet.go new file mode 100644 index 000000000000..f19039737281 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example10/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.CreateRequestA{ + Name: "name", + Value: 1, + } + client.Endpoints.DuplicateNamesA.Create( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example11/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example11/snippet.go new file mode 100644 index 000000000000..df421a1d4b74 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example11/snippet.go @@ -0,0 +1,29 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetRequestA{ + Id: "id", + Filter: fern.String( + "filter", + ), + } + client.Endpoints.DuplicateNamesA.Get( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example12/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example12/snippet.go new file mode 100644 index 000000000000..7c4082bad019 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example12/snippet.go @@ -0,0 +1,31 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.ListRequestA{ + Page: fern.Int( + 1, + ), + Limit: fern.Int( + 1, + ), + } + client.Endpoints.DuplicateNamesA.List( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example13/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example13/snippet.go new file mode 100644 index 000000000000..b05c679625a7 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example13/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.CreateRequestB{ + Description: "description", + Count: 1, + } + client.Endpoints.DuplicateNamesB.Create( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example14/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example14/snippet.go new file mode 100644 index 000000000000..d077562171a1 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example14/snippet.go @@ -0,0 +1,29 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetRequestB{ + Id: "id", + Expand: fern.Bool( + true, + ), + } + client.Endpoints.DuplicateNamesB.Get( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example15/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example15/snippet.go new file mode 100644 index 000000000000..6661b938e4e5 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example15/snippet.go @@ -0,0 +1,31 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.ListRequestB{ + Cursor: fern.String( + "cursor", + ), + Size: fern.Int( + 1, + ), + } + client.Endpoints.DuplicateNamesB.List( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example16/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example16/snippet.go new file mode 100644 index 000000000000..927263426700 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example16/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.CreateRequestC{ + Label: "label", + Priority: 1, + } + client.Endpoints.DuplicateNamesC.Create( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example17/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example17/snippet.go new file mode 100644 index 000000000000..b7b59ef0335a --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example17/snippet.go @@ -0,0 +1,29 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetRequestC{ + Id: "id", + Verbose: fern.Bool( + true, + ), + } + client.Endpoints.DuplicateNamesC.Get( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example18/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example18/snippet.go new file mode 100644 index 000000000000..6c6dc2da1e5b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example18/snippet.go @@ -0,0 +1,31 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.ListRequestC{ + Offset: fern.Int( + 1, + ), + Count: fern.Int( + 1, + ), + } + client.Endpoints.DuplicateNamesC.List( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example19/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example19/snippet.go new file mode 100644 index 000000000000..5cf921459a86 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example19/snippet.go @@ -0,0 +1,24 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := types.WeatherReportSunny.Ptr() + client.Endpoints.Enum.GetAndReturnEnum( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example2/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example2/snippet.go new file mode 100644 index 000000000000..7cb8a1226642 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example2/snippet.go @@ -0,0 +1,25 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := []string{ + "string", + } + client.Endpoints.Container.GetAndReturnSetOfPrimitives( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example20/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example20/snippet.go new file mode 100644 index 000000000000..d08078661ddc --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example20/snippet.go @@ -0,0 +1,22 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.Endpoints.HttpMethods.TestGet( + context.TODO(), + "id", + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example21/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example21/snippet.go new file mode 100644 index 000000000000..332c5ae7982d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example21/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + client.Endpoints.HttpMethods.TestPost( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example22/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example22/snippet.go new file mode 100644 index 000000000000..55f8f10f39d5 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example22/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + client.Endpoints.HttpMethods.TestPut( + context.TODO(), + "id", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example23/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example23/snippet.go new file mode 100644 index 000000000000..00bdb9e5026a --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example23/snippet.go @@ -0,0 +1,74 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + client.Endpoints.HttpMethods.TestPatch( + context.TODO(), + "id", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example24/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example24/snippet.go new file mode 100644 index 000000000000..5c056e831c6f --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example24/snippet.go @@ -0,0 +1,22 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.Endpoints.HttpMethods.TestDelete( + context.TODO(), + "id", + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example25/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example25/snippet.go new file mode 100644 index 000000000000..bc8a9a711f5c --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example25/snippet.go @@ -0,0 +1,73 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + client.Endpoints.Object.GetAndReturnWithOptionalField( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example26/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example26/snippet.go new file mode 100644 index 000000000000..5cf9b9aafb0b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example26/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + client.Endpoints.Object.GetAndReturnWithRequiredField( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example27/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example27/snippet.go new file mode 100644 index 000000000000..a02f9ee2bc11 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example27/snippet.go @@ -0,0 +1,30 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithMapOfMap{ + Map: map[string]map[string]string{ + "map": map[string]string{ + "map": "map", + }, + }, + } + client.Endpoints.Object.GetAndReturnWithMapOfMap( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example28/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example28/snippet.go new file mode 100644 index 000000000000..79038993507a --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example28/snippet.go @@ -0,0 +1,78 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.NestedObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } + client.Endpoints.Object.GetAndReturnNestedWithOptionalField( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example29/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example29/snippet.go new file mode 100644 index 000000000000..2f20e2c60937 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example29/snippet.go @@ -0,0 +1,77 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } + client.Endpoints.Object.GetAndReturnNestedWithRequiredField( + context.TODO(), + "string", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example3/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example3/snippet.go new file mode 100644 index 000000000000..811f79ec5a1b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example3/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := []*types.ObjectWithRequiredField{ + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } + client.Endpoints.Container.GetAndReturnSetOfObjects( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example30/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example30/snippet.go new file mode 100644 index 000000000000..e10c0876407d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example30/snippet.go @@ -0,0 +1,129 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := []*types.NestedObjectWithRequiredField{ + &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + }, + &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + }, + } + client.Endpoints.Object.GetAndReturnNestedWithRequiredFieldAsList( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example31/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example31/snippet.go new file mode 100644 index 000000000000..fc79e8ccb8fb --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example31/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithUnknownField{ + Unknown: map[string]any{ + "$ref": "https://example.com/schema", + }, + } + client.Endpoints.Object.GetAndReturnWithUnknownField( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example32/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example32/snippet.go new file mode 100644 index 000000000000..36a351c25c47 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example32/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithUnknownField{ + Unknown: map[string]any{ + "key": "value", + }, + } + client.Endpoints.Object.GetAndReturnWithUnknownField( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example33/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example33/snippet.go new file mode 100644 index 000000000000..df2fc7d06c3d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example33/snippet.go @@ -0,0 +1,30 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithDatetimeLikeString{ + DatetimeLikeString: "2023-08-31T14:15:22Z", + ActualDatetime: fern.MustParseDateTime( + "2023-08-31T14:15:22Z", + ), + } + client.Endpoints.Object.GetAndReturnWithDatetimeLikeString( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example34/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example34/snippet.go new file mode 100644 index 000000000000..8fa6d18950dd --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example34/snippet.go @@ -0,0 +1,30 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithDatetimeLikeString{ + DatetimeLikeString: "datetimeLikeString", + ActualDatetime: fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + } + client.Endpoints.Object.GetAndReturnWithDatetimeLikeString( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example35/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example35/snippet.go new file mode 100644 index 000000000000..98cd0f431b45 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example35/snippet.go @@ -0,0 +1,31 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.ListItemsRequest{ + Cursor: fern.String( + "cursor", + ), + Limit: fern.Int( + 1, + ), + } + client.Endpoints.Pagination.ListItems( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example36/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example36/snippet.go new file mode 100644 index 000000000000..dfff5b341c07 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example36/snippet.go @@ -0,0 +1,22 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.Endpoints.Params.GetWithPath( + context.TODO(), + "param", + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example37/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example37/snippet.go new file mode 100644 index 000000000000..dfff5b341c07 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example37/snippet.go @@ -0,0 +1,22 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.Endpoints.Params.GetWithPath( + context.TODO(), + "param", + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example38/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example38/snippet.go new file mode 100644 index 000000000000..5858866234be --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example38/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetWithQuery{ + Query: "query", + Number: 1, + } + client.Endpoints.Params.GetWithQuery( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example39/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example39/snippet.go new file mode 100644 index 000000000000..5858866234be --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example39/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetWithQuery{ + Query: "query", + Number: 1, + } + client.Endpoints.Params.GetWithQuery( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example4/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example4/snippet.go new file mode 100644 index 000000000000..65dd350f6278 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example4/snippet.go @@ -0,0 +1,25 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := map[string]string{ + "string": "string", + } + client.Endpoints.Container.GetAndReturnMapPrimToPrim( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example40/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example40/snippet.go new file mode 100644 index 000000000000..70ccb9272f36 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example40/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetWithPathAndQuery{ + Query: "query", + } + client.Endpoints.Params.GetWithPathAndQuery( + context.TODO(), + "param", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example41/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example41/snippet.go new file mode 100644 index 000000000000..70ccb9272f36 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example41/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.GetWithPathAndQuery{ + Query: "query", + } + client.Endpoints.Params.GetWithPathAndQuery( + context.TODO(), + "param", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example42/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example42/snippet.go new file mode 100644 index 000000000000..8ae1addc35d6 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example42/snippet.go @@ -0,0 +1,24 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := "string" + client.Endpoints.Params.ModifyWithPath( + context.TODO(), + "param", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example43/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example43/snippet.go new file mode 100644 index 000000000000..8ae1addc35d6 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example43/snippet.go @@ -0,0 +1,24 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := "string" + client.Endpoints.Params.ModifyWithPath( + context.TODO(), + "param", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example44/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example44/snippet.go new file mode 100644 index 000000000000..8074e7b5975c --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example44/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + bytes "bytes" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := bytes.NewReader( + []byte(""), + ) + client.Endpoints.Params.UploadWithPath( + context.TODO(), + "upload-path", + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example45/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example45/snippet.go new file mode 100644 index 000000000000..34601446d022 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example45/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := "string" + client.Endpoints.Primitive.GetAndReturnString( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example46/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example46/snippet.go new file mode 100644 index 000000000000..cbbc8a1abebe --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example46/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := 1 + client.Endpoints.Primitive.GetAndReturnInt( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example47/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example47/snippet.go new file mode 100644 index 000000000000..b1bd8c35ced3 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example47/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := int64(1000000) + client.Endpoints.Primitive.GetAndReturnLong( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example48/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example48/snippet.go new file mode 100644 index 000000000000..7953f38f88c0 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example48/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := 1.1 + client.Endpoints.Primitive.GetAndReturnDouble( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example49/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example49/snippet.go new file mode 100644 index 000000000000..e1a211b05df5 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example49/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := true + client.Endpoints.Primitive.GetAndReturnBool( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example5/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example5/snippet.go new file mode 100644 index 000000000000..2e6656adb75e --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example5/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := map[string]*types.ObjectWithRequiredField{ + "string": &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } + client.Endpoints.Container.GetAndReturnMapOfPrimToObject( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example50/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example50/snippet.go new file mode 100644 index 000000000000..e0990e920246 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example50/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ) + client.Endpoints.Primitive.GetAndReturnDatetime( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example51/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example51/snippet.go new file mode 100644 index 000000000000..c1069f1a6783 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example51/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := fern.MustParseDate( + "2023-01-15", + ) + client.Endpoints.Primitive.GetAndReturnDate( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example52/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example52/snippet.go new file mode 100644 index 000000000000..fd12ac2a60a3 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example52/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ) + client.Endpoints.Primitive.GetAndReturnUuid( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example53/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example53/snippet.go new file mode 100644 index 000000000000..e65a7a66183b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example53/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := []byte("SGVsbG8gd29ybGQh") + client.Endpoints.Primitive.GetAndReturnBase64( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example54/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example54/snippet.go new file mode 100644 index 000000000000..a38b38995190 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example54/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.PutRequest{ + Id: "id", + } + client.Endpoints.Put.Add( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example55/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example55/snippet.go new file mode 100644 index 000000000000..f7a53c997592 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example55/snippet.go @@ -0,0 +1,29 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.Animal{ + Dog: &types.Dog{ + Name: "name", + LikesToWoof: true, + }, + } + client.Endpoints.Union.GetAndReturnUnion( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/exhaustive/no-custom-config/dynamic-snippets/example58/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example56/snippet.go similarity index 100% rename from seed/go-sdk/exhaustive/no-custom-config/dynamic-snippets/example58/snippet.go rename to seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example56/snippet.go diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example57/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example57/snippet.go new file mode 100644 index 000000000000..54e123f11b95 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example57/snippet.go @@ -0,0 +1,21 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.Endpoints.Urls.NoEndingSlash( + context.TODO(), + ) +} diff --git a/seed/go-sdk/exhaustive/omit-empty-request-wrappers/dynamic-snippets/example58/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example58/snippet.go similarity index 100% rename from seed/go-sdk/exhaustive/omit-empty-request-wrappers/dynamic-snippets/example58/snippet.go rename to seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example58/snippet.go diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example59/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example59/snippet.go new file mode 100644 index 000000000000..278d95e3aca5 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example59/snippet.go @@ -0,0 +1,21 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.Endpoints.Urls.WithUnderscores( + context.TODO(), + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example6/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example6/snippet.go new file mode 100644 index 000000000000..46c2c49e9f1b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example6/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := map[string]*types.MixedType{ + "string": &types.MixedType{ + Double: 1.1, + }, + } + client.Endpoints.Container.GetAndReturnMapOfPrimToUndiscriminatedUnion( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example60/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example60/snippet.go new file mode 100644 index 000000000000..1793d7d25014 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example60/snippet.go @@ -0,0 +1,77 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + types "github.com/go-deterministic-ordering/fern/types" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.PostWithObjectBody{ + FieldString: "string", + Integer: 1, + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } + client.InlinedRequests.PostWithObjectBodyandResponse( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example61/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example61/snippet.go new file mode 100644 index 000000000000..1793d7d25014 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example61/snippet.go @@ -0,0 +1,77 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + types "github.com/go-deterministic-ordering/fern/types" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.PostWithObjectBody{ + FieldString: "string", + Integer: 1, + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } + client.InlinedRequests.PostWithObjectBodyandResponse( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example62/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example62/snippet.go new file mode 100644 index 000000000000..ae6b011ce074 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example62/snippet.go @@ -0,0 +1,25 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := map[string]any{ + "key": "value", + } + client.NoAuth.PostWithNoAuth( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example63/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example63/snippet.go new file mode 100644 index 000000000000..ae6b011ce074 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example63/snippet.go @@ -0,0 +1,25 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := map[string]any{ + "key": "value", + } + client.NoAuth.PostWithNoAuth( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example64/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example64/snippet.go new file mode 100644 index 000000000000..c2e4f522a74b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example64/snippet.go @@ -0,0 +1,21 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.NoReqBody.GetWithNoRequestBody( + context.TODO(), + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example65/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example65/snippet.go new file mode 100644 index 000000000000..e928db50ad7d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example65/snippet.go @@ -0,0 +1,21 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + client.NoReqBody.PostWithNoRequestBody( + context.TODO(), + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example66/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example66/snippet.go new file mode 100644 index 000000000000..ca4694b89602 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example66/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + fern "github.com/go-deterministic-ordering/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.ReqWithHeaders{ + XTestServiceHeader: "X-TEST-SERVICE-HEADER", + XTestEndpointHeader: "X-TEST-ENDPOINT-HEADER", + Body: "string", + } + client.ReqWithHeaders.GetWithCustomHeader( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example7/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example7/snippet.go new file mode 100644 index 000000000000..84f835fe637f --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example7/snippet.go @@ -0,0 +1,26 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + client.Endpoints.Container.GetAndReturnOptional( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example8/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example8/snippet.go new file mode 100644 index 000000000000..db01086e61d2 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example8/snippet.go @@ -0,0 +1,73 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + client.Endpoints.ContentType.PostJsonPatchContentType( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example9/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example9/snippet.go new file mode 100644 index 000000000000..a192996af03d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example9/snippet.go @@ -0,0 +1,73 @@ +package example + +import ( + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + fern "github.com/go-deterministic-ordering/fern" + uuid "github.com/google/uuid" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + client.Endpoints.ContentType.PostJsonPatchContentWithCharsetType( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/client/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/client/client.go new file mode 100644 index 000000000000..4a9ea17ee601 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/client/client.go @@ -0,0 +1,70 @@ +// Code generated by Fern. DO NOT EDIT. + +package client + +import ( + core "github.com/go-deterministic-ordering/fern/core" + container "github.com/go-deterministic-ordering/fern/endpoints/container" + contenttype "github.com/go-deterministic-ordering/fern/endpoints/contenttype" + duplicatenamesa "github.com/go-deterministic-ordering/fern/endpoints/duplicatenamesa" + duplicatenamesb "github.com/go-deterministic-ordering/fern/endpoints/duplicatenamesb" + duplicatenamesc "github.com/go-deterministic-ordering/fern/endpoints/duplicatenamesc" + enum "github.com/go-deterministic-ordering/fern/endpoints/enum" + httpmethods "github.com/go-deterministic-ordering/fern/endpoints/httpmethods" + object "github.com/go-deterministic-ordering/fern/endpoints/object" + pagination "github.com/go-deterministic-ordering/fern/endpoints/pagination" + params "github.com/go-deterministic-ordering/fern/endpoints/params" + primitive "github.com/go-deterministic-ordering/fern/endpoints/primitive" + put "github.com/go-deterministic-ordering/fern/endpoints/put" + union "github.com/go-deterministic-ordering/fern/endpoints/union" + urls "github.com/go-deterministic-ordering/fern/endpoints/urls" + internal "github.com/go-deterministic-ordering/fern/internal" +) + +type Client struct { + Container *container.Client + ContentType *contenttype.Client + DuplicateNamesA *duplicatenamesa.Client + DuplicateNamesB *duplicatenamesb.Client + DuplicateNamesC *duplicatenamesc.Client + Enum *enum.Client + HttpMethods *httpmethods.Client + Object *object.Client + Pagination *pagination.Client + Params *params.Client + Primitive *primitive.Client + Put *put.Client + Union *union.Client + Urls *urls.Client + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + Container: container.NewClient(options), + ContentType: contenttype.NewClient(options), + DuplicateNamesA: duplicatenamesa.NewClient(options), + DuplicateNamesB: duplicatenamesb.NewClient(options), + DuplicateNamesC: duplicatenamesc.NewClient(options), + Enum: enum.NewClient(options), + HttpMethods: httpmethods.NewClient(options), + Object: object.NewClient(options), + Pagination: pagination.NewClient(options), + Params: params.NewClient(options), + Primitive: primitive.NewClient(options), + Put: put.NewClient(options), + Union: union.NewClient(options), + Urls: urls.NewClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/container/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/container/client.go new file mode 100644 index 000000000000..7017a76556b8 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/container/client.go @@ -0,0 +1,161 @@ +// Code generated by Fern. DO NOT EDIT. + +package container + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) GetAndReturnListOfPrimitives( + ctx context.Context, + request []string, + opts ...option.RequestOption, +) ([]string, error) { + response, err := c.WithRawResponse.GetAndReturnListOfPrimitives( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnListOfObjects( + ctx context.Context, + request []*types.ObjectWithRequiredField, + opts ...option.RequestOption, +) ([]*types.ObjectWithRequiredField, error) { + response, err := c.WithRawResponse.GetAndReturnListOfObjects( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnSetOfPrimitives( + ctx context.Context, + request []string, + opts ...option.RequestOption, +) ([]string, error) { + response, err := c.WithRawResponse.GetAndReturnSetOfPrimitives( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnSetOfObjects( + ctx context.Context, + request []*types.ObjectWithRequiredField, + opts ...option.RequestOption, +) ([]*types.ObjectWithRequiredField, error) { + response, err := c.WithRawResponse.GetAndReturnSetOfObjects( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnMapPrimToPrim( + ctx context.Context, + request map[string]string, + opts ...option.RequestOption, +) (map[string]string, error) { + response, err := c.WithRawResponse.GetAndReturnMapPrimToPrim( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnMapOfPrimToObject( + ctx context.Context, + request map[string]*types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (map[string]*types.ObjectWithRequiredField, error) { + response, err := c.WithRawResponse.GetAndReturnMapOfPrimToObject( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnMapOfPrimToUndiscriminatedUnion( + ctx context.Context, + request map[string]*types.MixedType, + opts ...option.RequestOption, +) (map[string]*types.MixedType, error) { + response, err := c.WithRawResponse.GetAndReturnMapOfPrimToUndiscriminatedUnion( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnOptional( + ctx context.Context, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*types.ObjectWithRequiredField, error) { + response, err := c.WithRawResponse.GetAndReturnOptional( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/container/endpoints_container_test/endpoints_container_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/container/endpoints_container_test/endpoints_container_test.go new file mode 100644 index 000000000000..c3f453759970 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/container/endpoints_container_test/endpoints_container_test.go @@ -0,0 +1,275 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_container_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsContainerGetAndReturnListOfPrimitivesWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := []string{ + "string", + "string", + } + _, invocationErr := client.Endpoints.Container.GetAndReturnListOfPrimitives( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnListOfPrimitivesWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnListOfPrimitivesWithWireMock", "POST", "/container/list-of-primitives", nil, 1) +} + +func TestEndpointsContainerGetAndReturnListOfObjectsWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := []*types.ObjectWithRequiredField{ + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } + _, invocationErr := client.Endpoints.Container.GetAndReturnListOfObjects( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnListOfObjectsWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnListOfObjectsWithWireMock", "POST", "/container/list-of-objects", nil, 1) +} + +func TestEndpointsContainerGetAndReturnSetOfPrimitivesWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := []string{ + "string", + } + _, invocationErr := client.Endpoints.Container.GetAndReturnSetOfPrimitives( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnSetOfPrimitivesWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnSetOfPrimitivesWithWireMock", "POST", "/container/set-of-primitives", nil, 1) +} + +func TestEndpointsContainerGetAndReturnSetOfObjectsWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := []*types.ObjectWithRequiredField{ + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } + _, invocationErr := client.Endpoints.Container.GetAndReturnSetOfObjects( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnSetOfObjectsWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnSetOfObjectsWithWireMock", "POST", "/container/set-of-objects", nil, 1) +} + +func TestEndpointsContainerGetAndReturnMapPrimToPrimWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := map[string]string{ + "string": "string", + } + _, invocationErr := client.Endpoints.Container.GetAndReturnMapPrimToPrim( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnMapPrimToPrimWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnMapPrimToPrimWithWireMock", "POST", "/container/map-prim-to-prim", nil, 1) +} + +func TestEndpointsContainerGetAndReturnMapOfPrimToObjectWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := map[string]*types.ObjectWithRequiredField{ + "string": &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } + _, invocationErr := client.Endpoints.Container.GetAndReturnMapOfPrimToObject( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnMapOfPrimToObjectWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnMapOfPrimToObjectWithWireMock", "POST", "/container/map-prim-to-object", nil, 1) +} + +func TestEndpointsContainerGetAndReturnMapOfPrimToUndiscriminatedUnionWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := map[string]*types.MixedType{ + "string": &types.MixedType{ + Double: 1.1, + }, + } + _, invocationErr := client.Endpoints.Container.GetAndReturnMapOfPrimToUndiscriminatedUnion( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnMapOfPrimToUndiscriminatedUnionWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnMapOfPrimToUndiscriminatedUnionWithWireMock", "POST", "/container/map-prim-to-union", nil, 1) +} + +func TestEndpointsContainerGetAndReturnOptionalWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + _, invocationErr := client.Endpoints.Container.GetAndReturnOptional( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContainerGetAndReturnOptionalWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContainerGetAndReturnOptionalWithWireMock", "POST", "/container/opt-objects", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/container/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/container/raw_client.go new file mode 100644 index 000000000000..294f2fd815a9 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/container/raw_client.go @@ -0,0 +1,359 @@ +// Code generated by Fern. DO NOT EDIT. + +package container + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetAndReturnListOfPrimitives( + ctx context.Context, + request []string, + opts ...option.RequestOption, +) (*core.Response[[]string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/list-of-primitives" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response []string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[[]string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnListOfObjects( + ctx context.Context, + request []*types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[[]*types.ObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/list-of-objects" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response []*types.ObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[[]*types.ObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnSetOfPrimitives( + ctx context.Context, + request []string, + opts ...option.RequestOption, +) (*core.Response[[]string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/set-of-primitives" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response []string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[[]string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnSetOfObjects( + ctx context.Context, + request []*types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[[]*types.ObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/set-of-objects" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response []*types.ObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[[]*types.ObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnMapPrimToPrim( + ctx context.Context, + request map[string]string, + opts ...option.RequestOption, +) (*core.Response[map[string]string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/map-prim-to-prim" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response map[string]string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[map[string]string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnMapOfPrimToObject( + ctx context.Context, + request map[string]*types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[map[string]*types.ObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/map-prim-to-object" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response map[string]*types.ObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[map[string]*types.ObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnMapOfPrimToUndiscriminatedUnion( + ctx context.Context, + request map[string]*types.MixedType, + opts ...option.RequestOption, +) (*core.Response[map[string]*types.MixedType], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/map-prim-to-union" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response map[string]*types.MixedType + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[map[string]*types.MixedType]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnOptional( + ctx context.Context, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/container/opt-objects" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/client.go new file mode 100644 index 000000000000..2cb6b96a5dcf --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/client.go @@ -0,0 +1,65 @@ +// Code generated by Fern. DO NOT EDIT. + +package contenttype + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) PostJsonPatchContentType( + ctx context.Context, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.PostJsonPatchContentType( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +func (c *Client) PostJsonPatchContentWithCharsetType( + ctx context.Context, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.PostJsonPatchContentWithCharsetType( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/endpoints_content_type_test/endpoints_content_type_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/endpoints_content_type_test/endpoints_content_type_test.go new file mode 100644 index 000000000000..7e8337e2bcec --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/endpoints_content_type_test/endpoints_content_type_test.go @@ -0,0 +1,205 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_content_type_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + uuid "github.com/google/uuid" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsContentTypePostJsonPatchContentTypeWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + invocationErr := client.Endpoints.ContentType.PostJsonPatchContentType( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContentTypePostJsonPatchContentTypeWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContentTypePostJsonPatchContentTypeWithWireMock", "POST", "/foo/bar", nil, 1) +} + +func TestEndpointsContentTypePostJsonPatchContentWithCharsetTypeWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + invocationErr := client.Endpoints.ContentType.PostJsonPatchContentWithCharsetType( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsContentTypePostJsonPatchContentWithCharsetTypeWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsContentTypePostJsonPatchContentWithCharsetTypeWithWireMock", "POST", "/foo/baz", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/raw_client.go new file mode 100644 index 000000000000..b3db8080c61d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/contenttype/raw_client.go @@ -0,0 +1,109 @@ +// Code generated by Fern. DO NOT EDIT. + +package contenttype + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) PostJsonPatchContentType( + ctx context.Context, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/foo/bar" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) PostJsonPatchContentWithCharsetType( + ctx context.Context, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/foo/baz" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/client.go new file mode 100644 index 000000000000..9299695a04d4 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/client.go @@ -0,0 +1,85 @@ +// Code generated by Fern. DO NOT EDIT. + +package duplicatenamesa + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// Create endpoint for service A +func (c *Client) Create( + ctx context.Context, + request *fern.CreateRequestA, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.Create( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +// Get endpoint for service A +func (c *Client) Get( + ctx context.Context, + request *fern.GetRequestA, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.Get( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +// List endpoint for service A +func (c *Client) List( + ctx context.Context, + request *fern.ListRequestA, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.List( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/endpoints_duplicate_names_a_test/endpoints_duplicate_names_a_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/endpoints_duplicate_names_a_test/endpoints_duplicate_names_a_test.go new file mode 100644 index 000000000000..58d35b1d7c55 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/endpoints_duplicate_names_a_test/endpoints_duplicate_names_a_test.go @@ -0,0 +1,147 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_duplicate_names_a_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsDuplicateNamesACreateWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.CreateRequestA{ + Name: "name", + Value: 1, + } + _, invocationErr := client.Endpoints.DuplicateNamesA.Create( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesACreateWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesACreateWithWireMock", "POST", "/duplicate-names-a", nil, 1) +} + +func TestEndpointsDuplicateNamesAGetWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.GetRequestA{ + Id: "id", + Filter: fern.String( + "filter", + ), + } + invocationErr := client.Endpoints.DuplicateNamesA.Get( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesAGetWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesAGetWithWireMock", "GET", "/duplicate-names-a/id", map[string]string{"filter": "filter"}, 1) +} + +func TestEndpointsDuplicateNamesAListWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.ListRequestA{ + Page: fern.Int( + 1, + ), + Limit: fern.Int( + 1, + ), + } + invocationErr := client.Endpoints.DuplicateNamesA.List( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesAListWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesAListWithWireMock", "GET", "/duplicate-names-a", map[string]string{"page": "1", "limit": "1"}, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/raw_client.go new file mode 100644 index 000000000000..26ba2c413997 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesa/raw_client.go @@ -0,0 +1,166 @@ +// Code generated by Fern. DO NOT EDIT. + +package duplicatenamesa + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) Create( + ctx context.Context, + request *fern.CreateRequestA, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/duplicate-names-a" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) Get( + ctx context.Context, + request *fern.GetRequestA, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/duplicate-names-a/%v", + request.Id, + ) + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) List( + ctx context.Context, + request *fern.ListRequestA, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/duplicate-names-a" + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/client.go new file mode 100644 index 000000000000..bf93b6e5f292 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/client.go @@ -0,0 +1,85 @@ +// Code generated by Fern. DO NOT EDIT. + +package duplicatenamesb + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// Create endpoint for service B +func (c *Client) Create( + ctx context.Context, + request *fern.CreateRequestB, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.Create( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +// Get endpoint for service B +func (c *Client) Get( + ctx context.Context, + request *fern.GetRequestB, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.Get( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +// List endpoint for service B +func (c *Client) List( + ctx context.Context, + request *fern.ListRequestB, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.List( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/endpoints_duplicate_names_b_test/endpoints_duplicate_names_b_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/endpoints_duplicate_names_b_test/endpoints_duplicate_names_b_test.go new file mode 100644 index 000000000000..0071a8bee909 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/endpoints_duplicate_names_b_test/endpoints_duplicate_names_b_test.go @@ -0,0 +1,147 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_duplicate_names_b_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsDuplicateNamesBCreateWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.CreateRequestB{ + Description: "description", + Count: 1, + } + _, invocationErr := client.Endpoints.DuplicateNamesB.Create( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesBCreateWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesBCreateWithWireMock", "POST", "/duplicate-names-b", nil, 1) +} + +func TestEndpointsDuplicateNamesBGetWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.GetRequestB{ + Id: "id", + Expand: fern.Bool( + true, + ), + } + invocationErr := client.Endpoints.DuplicateNamesB.Get( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesBGetWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesBGetWithWireMock", "GET", "/duplicate-names-b/id", map[string]string{"expand": "true"}, 1) +} + +func TestEndpointsDuplicateNamesBListWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.ListRequestB{ + Cursor: fern.String( + "cursor", + ), + Size: fern.Int( + 1, + ), + } + invocationErr := client.Endpoints.DuplicateNamesB.List( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesBListWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesBListWithWireMock", "GET", "/duplicate-names-b", map[string]string{"cursor": "cursor", "size": "1"}, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/raw_client.go new file mode 100644 index 000000000000..e69cab64459d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesb/raw_client.go @@ -0,0 +1,166 @@ +// Code generated by Fern. DO NOT EDIT. + +package duplicatenamesb + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) Create( + ctx context.Context, + request *fern.CreateRequestB, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/duplicate-names-b" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) Get( + ctx context.Context, + request *fern.GetRequestB, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/duplicate-names-b/%v", + request.Id, + ) + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) List( + ctx context.Context, + request *fern.ListRequestB, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/duplicate-names-b" + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/client.go new file mode 100644 index 000000000000..fde42b80affa --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/client.go @@ -0,0 +1,85 @@ +// Code generated by Fern. DO NOT EDIT. + +package duplicatenamesc + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// Create endpoint for service C +func (c *Client) Create( + ctx context.Context, + request *fern.CreateRequestC, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.Create( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +// Get endpoint for service C +func (c *Client) Get( + ctx context.Context, + request *fern.GetRequestC, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.Get( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +// List endpoint for service C +func (c *Client) List( + ctx context.Context, + request *fern.ListRequestC, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.List( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/endpoints_duplicate_names_c_test/endpoints_duplicate_names_c_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/endpoints_duplicate_names_c_test/endpoints_duplicate_names_c_test.go new file mode 100644 index 000000000000..f60afc6bef8f --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/endpoints_duplicate_names_c_test/endpoints_duplicate_names_c_test.go @@ -0,0 +1,147 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_duplicate_names_c_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsDuplicateNamesCCreateWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.CreateRequestC{ + Label: "label", + Priority: 1, + } + _, invocationErr := client.Endpoints.DuplicateNamesC.Create( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesCCreateWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesCCreateWithWireMock", "POST", "/duplicate-names-c", nil, 1) +} + +func TestEndpointsDuplicateNamesCGetWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.GetRequestC{ + Id: "id", + Verbose: fern.Bool( + true, + ), + } + invocationErr := client.Endpoints.DuplicateNamesC.Get( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesCGetWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesCGetWithWireMock", "GET", "/duplicate-names-c/id", map[string]string{"verbose": "true"}, 1) +} + +func TestEndpointsDuplicateNamesCListWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.ListRequestC{ + Offset: fern.Int( + 1, + ), + Count: fern.Int( + 1, + ), + } + invocationErr := client.Endpoints.DuplicateNamesC.List( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsDuplicateNamesCListWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsDuplicateNamesCListWithWireMock", "GET", "/duplicate-names-c", map[string]string{"offset": "1", "count": "1"}, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/raw_client.go new file mode 100644 index 000000000000..b88d3cf4fd1b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/duplicatenamesc/raw_client.go @@ -0,0 +1,166 @@ +// Code generated by Fern. DO NOT EDIT. + +package duplicatenamesc + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) Create( + ctx context.Context, + request *fern.CreateRequestC, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/duplicate-names-c" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) Get( + ctx context.Context, + request *fern.GetRequestC, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/duplicate-names-c/%v", + request.Id, + ) + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) List( + ctx context.Context, + request *fern.ListRequestC, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/duplicate-names-c" + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/enum/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/enum/client.go new file mode 100644 index 000000000000..7f6c3fc27e02 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/enum/client.go @@ -0,0 +1,49 @@ +// Code generated by Fern. DO NOT EDIT. + +package enum + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) GetAndReturnEnum( + ctx context.Context, + request *types.WeatherReport, + opts ...option.RequestOption, +) (*types.WeatherReport, error) { + response, err := c.WithRawResponse.GetAndReturnEnum( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/enum/endpoints_enum_test/endpoints_enum_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/enum/endpoints_enum_test/endpoints_enum_test.go new file mode 100644 index 000000000000..440b5431b67a --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/enum/endpoints_enum_test/endpoints_enum_test.go @@ -0,0 +1,86 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_enum_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsEnumGetAndReturnEnumWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := types.WeatherReportSunny.Ptr() + _, invocationErr := client.Endpoints.Enum.GetAndReturnEnum( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsEnumGetAndReturnEnumWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsEnumGetAndReturnEnumWithWireMock", "POST", "/enum", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/enum/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/enum/raw_client.go new file mode 100644 index 000000000000..26ddc60186d0 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/enum/raw_client.go @@ -0,0 +1,72 @@ +// Code generated by Fern. DO NOT EDIT. + +package enum + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetAndReturnEnum( + ctx context.Context, + request *types.WeatherReport, + opts ...option.RequestOption, +) (*core.Response[*types.WeatherReport], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/enum" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.WeatherReport + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.WeatherReport]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/error_codes.go b/seed/go-sdk/go-deterministic-ordering/endpoints/error_codes.go new file mode 100644 index 000000000000..49fc58006705 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/error_codes.go @@ -0,0 +1,9 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints + +import ( + internal "github.com/go-deterministic-ordering/fern/internal" +) + +var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/client.go new file mode 100644 index 000000000000..2445ede416bf --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/client.go @@ -0,0 +1,117 @@ +// Code generated by Fern. DO NOT EDIT. + +package httpmethods + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) TestGet( + ctx context.Context, + id string, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.TestGet( + ctx, + id, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +func (c *Client) TestPost( + ctx context.Context, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.TestPost( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) TestPut( + ctx context.Context, + id string, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.TestPut( + ctx, + id, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) TestPatch( + ctx context.Context, + id string, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.TestPatch( + ctx, + id, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) TestDelete( + ctx context.Context, + id string, + opts ...option.RequestOption, +) (bool, error) { + response, err := c.WithRawResponse.TestDelete( + ctx, + id, + opts..., + ) + if err != nil { + return false, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/endpoints_http_methods_test/endpoints_http_methods_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/endpoints_http_methods_test/endpoints_http_methods_test.go new file mode 100644 index 000000000000..1bde0604c2fe --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/endpoints_http_methods_test/endpoints_http_methods_test.go @@ -0,0 +1,231 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_http_methods_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + uuid "github.com/google/uuid" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsHttpMethodsTestGetWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.HttpMethods.TestGet( + context.TODO(), + "id", + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsHttpMethodsTestGetWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsHttpMethodsTestGetWithWireMock", "GET", "/http-methods/id", nil, 1) +} + +func TestEndpointsHttpMethodsTestPostWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + _, invocationErr := client.Endpoints.HttpMethods.TestPost( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsHttpMethodsTestPostWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsHttpMethodsTestPostWithWireMock", "POST", "/http-methods", nil, 1) +} + +func TestEndpointsHttpMethodsTestPutWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + _, invocationErr := client.Endpoints.HttpMethods.TestPut( + context.TODO(), + "id", + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsHttpMethodsTestPutWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsHttpMethodsTestPutWithWireMock", "PUT", "/http-methods/id", nil, 1) +} + +func TestEndpointsHttpMethodsTestPatchWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + _, invocationErr := client.Endpoints.HttpMethods.TestPatch( + context.TODO(), + "id", + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsHttpMethodsTestPatchWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsHttpMethodsTestPatchWithWireMock", "PATCH", "/http-methods/id", nil, 1) +} + +func TestEndpointsHttpMethodsTestDeleteWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.HttpMethods.TestDelete( + context.TODO(), + "id", + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsHttpMethodsTestDeleteWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsHttpMethodsTestDeleteWithWireMock", "DELETE", "/http-methods/id", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/raw_client.go new file mode 100644 index 000000000000..0315d94e2254 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/httpmethods/raw_client.go @@ -0,0 +1,248 @@ +// Code generated by Fern. DO NOT EDIT. + +package httpmethods + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) TestGet( + ctx context.Context, + id string, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/http-methods/%v", + id, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) TestPost( + ctx context.Context, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/http-methods" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) TestPut( + ctx context.Context, + id string, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/http-methods/%v", + id, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPut, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) TestPatch( + ctx context.Context, + id string, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/http-methods/%v", + id, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPatch, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) TestDelete( + ctx context.Context, + id string, + opts ...option.RequestOption, +) (*core.Response[bool], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/http-methods/%v", + id, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response bool + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodDelete, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[bool]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/object/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/object/client.go new file mode 100644 index 000000000000..cb8638d91fcb --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/object/client.go @@ -0,0 +1,166 @@ +// Code generated by Fern. DO NOT EDIT. + +package object + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) GetAndReturnWithOptionalField( + ctx context.Context, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.GetAndReturnWithOptionalField( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnWithRequiredField( + ctx context.Context, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*types.ObjectWithRequiredField, error) { + response, err := c.WithRawResponse.GetAndReturnWithRequiredField( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnWithMapOfMap( + ctx context.Context, + request *types.ObjectWithMapOfMap, + opts ...option.RequestOption, +) (*types.ObjectWithMapOfMap, error) { + response, err := c.WithRawResponse.GetAndReturnWithMapOfMap( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnNestedWithOptionalField( + ctx context.Context, + request *types.NestedObjectWithOptionalField, + opts ...option.RequestOption, +) (*types.NestedObjectWithOptionalField, error) { + response, err := c.WithRawResponse.GetAndReturnNestedWithOptionalField( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnNestedWithRequiredField( + ctx context.Context, + string_ string, + request *types.NestedObjectWithRequiredField, + opts ...option.RequestOption, +) (*types.NestedObjectWithRequiredField, error) { + response, err := c.WithRawResponse.GetAndReturnNestedWithRequiredField( + ctx, + string_, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnNestedWithRequiredFieldAsList( + ctx context.Context, + request []*types.NestedObjectWithRequiredField, + opts ...option.RequestOption, +) (*types.NestedObjectWithRequiredField, error) { + response, err := c.WithRawResponse.GetAndReturnNestedWithRequiredFieldAsList( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnWithUnknownField( + ctx context.Context, + request *types.ObjectWithUnknownField, + opts ...option.RequestOption, +) (*types.ObjectWithUnknownField, error) { + response, err := c.WithRawResponse.GetAndReturnWithUnknownField( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +// Tests that string fields containing datetime-like values are NOT reformatted. +// The datetimeLikeString field should preserve its exact value "2023-08-31T14:15:22Z" +// without being converted to "2023-08-31T14:15:22.000Z". +func (c *Client) GetAndReturnWithDatetimeLikeString( + ctx context.Context, + request *types.ObjectWithDatetimeLikeString, + opts ...option.RequestOption, +) (*types.ObjectWithDatetimeLikeString, error) { + response, err := c.WithRawResponse.GetAndReturnWithDatetimeLikeString( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/object/endpoints_object_test/endpoints_object_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/object/endpoints_object_test/endpoints_object_test.go new file mode 100644 index 000000000000..f1a41265e860 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/object/endpoints_object_test/endpoints_object_test.go @@ -0,0 +1,519 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_object_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + uuid "github.com/google/uuid" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsObjectGetAndReturnWithOptionalFieldWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } + _, invocationErr := client.Endpoints.Object.GetAndReturnWithOptionalField( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnWithOptionalFieldWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnWithOptionalFieldWithWireMock", "POST", "/object/get-and-return-with-optional-field", nil, 1) +} + +func TestEndpointsObjectGetAndReturnWithRequiredFieldWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithRequiredField{ + FieldString: "string", + } + _, invocationErr := client.Endpoints.Object.GetAndReturnWithRequiredField( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnWithRequiredFieldWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnWithRequiredFieldWithWireMock", "POST", "/object/get-and-return-with-required-field", nil, 1) +} + +func TestEndpointsObjectGetAndReturnWithMapOfMapWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithMapOfMap{ + Map: map[string]map[string]string{ + "map": map[string]string{ + "map": "map", + }, + }, + } + _, invocationErr := client.Endpoints.Object.GetAndReturnWithMapOfMap( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnWithMapOfMapWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnWithMapOfMapWithWireMock", "POST", "/object/get-and-return-with-map-of-map", nil, 1) +} + +func TestEndpointsObjectGetAndReturnNestedWithOptionalFieldWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.NestedObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } + _, invocationErr := client.Endpoints.Object.GetAndReturnNestedWithOptionalField( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnNestedWithOptionalFieldWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnNestedWithOptionalFieldWithWireMock", "POST", "/object/get-and-return-nested-with-optional-field", nil, 1) +} + +func TestEndpointsObjectGetAndReturnNestedWithRequiredFieldWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } + _, invocationErr := client.Endpoints.Object.GetAndReturnNestedWithRequiredField( + context.TODO(), + "string", + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnNestedWithRequiredFieldWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnNestedWithRequiredFieldWithWireMock", "POST", "/object/get-and-return-nested-with-required-field/string", nil, 1) +} + +func TestEndpointsObjectGetAndReturnNestedWithRequiredFieldAsListWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := []*types.NestedObjectWithRequiredField{ + &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + }, + &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + }, + } + _, invocationErr := client.Endpoints.Object.GetAndReturnNestedWithRequiredFieldAsList( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnNestedWithRequiredFieldAsListWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnNestedWithRequiredFieldAsListWithWireMock", "POST", "/object/get-and-return-nested-with-required-field-list", nil, 1) +} + +func TestEndpointsObjectGetAndReturnWithUnknownFieldWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithUnknownField{ + Unknown: map[string]any{ + "$ref": "https://example.com/schema", + }, + } + _, invocationErr := client.Endpoints.Object.GetAndReturnWithUnknownField( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnWithUnknownFieldWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnWithUnknownFieldWithWireMock", "POST", "/object/get-and-return-with-unknown-field", nil, 1) +} + +func TestEndpointsObjectGetAndReturnWithDatetimeLikeStringWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.ObjectWithDatetimeLikeString{ + DatetimeLikeString: "2023-08-31T14:15:22Z", + ActualDatetime: fern.MustParseDateTime( + "2023-08-31T14:15:22Z", + ), + } + _, invocationErr := client.Endpoints.Object.GetAndReturnWithDatetimeLikeString( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsObjectGetAndReturnWithDatetimeLikeStringWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsObjectGetAndReturnWithDatetimeLikeStringWithWireMock", "POST", "/object/get-and-return-with-datetime-like-string", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/object/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/object/raw_client.go new file mode 100644 index 000000000000..72d3062f9b1b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/object/raw_client.go @@ -0,0 +1,363 @@ +// Code generated by Fern. DO NOT EDIT. + +package object + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetAndReturnWithOptionalField( + ctx context.Context, + request *types.ObjectWithOptionalField, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/object/get-and-return-with-optional-field" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnWithRequiredField( + ctx context.Context, + request *types.ObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/object/get-and-return-with-required-field" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnWithMapOfMap( + ctx context.Context, + request *types.ObjectWithMapOfMap, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithMapOfMap], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/object/get-and-return-with-map-of-map" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithMapOfMap + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithMapOfMap]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnNestedWithOptionalField( + ctx context.Context, + request *types.NestedObjectWithOptionalField, + opts ...option.RequestOption, +) (*core.Response[*types.NestedObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/object/get-and-return-nested-with-optional-field" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.NestedObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.NestedObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnNestedWithRequiredField( + ctx context.Context, + string_ string, + request *types.NestedObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[*types.NestedObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/object/get-and-return-nested-with-required-field/%v", + string_, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.NestedObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.NestedObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnNestedWithRequiredFieldAsList( + ctx context.Context, + request []*types.NestedObjectWithRequiredField, + opts ...option.RequestOption, +) (*core.Response[*types.NestedObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/object/get-and-return-nested-with-required-field-list" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.NestedObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.NestedObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnWithUnknownField( + ctx context.Context, + request *types.ObjectWithUnknownField, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithUnknownField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/object/get-and-return-with-unknown-field" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithUnknownField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithUnknownField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnWithDatetimeLikeString( + ctx context.Context, + request *types.ObjectWithDatetimeLikeString, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithDatetimeLikeString], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/object/get-and-return-with-datetime-like-string" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithDatetimeLikeString + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithDatetimeLikeString]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/pagination.go b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination.go new file mode 100644 index 000000000000..6e0a869ccb26 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination.go @@ -0,0 +1,111 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + types "github.com/go-deterministic-ordering/fern/types" + big "math/big" +) + +var ( + paginatedResponseFieldItems = big.NewInt(1 << 0) + paginatedResponseFieldNext = big.NewInt(1 << 1) +) + +type PaginatedResponse struct { + Items []*types.ObjectWithRequiredField `json:"items" url:"items"` + Next *string `json:"next,omitempty" url:"next,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (p *PaginatedResponse) GetItems() []*types.ObjectWithRequiredField { + if p == nil { + return nil + } + return p.Items +} + +func (p *PaginatedResponse) GetNext() *string { + if p == nil { + return nil + } + return p.Next +} + +func (p *PaginatedResponse) GetExtraProperties() map[string]interface{} { + if p == nil { + return nil + } + return p.extraProperties +} + +func (p *PaginatedResponse) require(field *big.Int) { + if p.explicitFields == nil { + p.explicitFields = big.NewInt(0) + } + p.explicitFields.Or(p.explicitFields, field) +} + +// SetItems sets the Items field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (p *PaginatedResponse) SetItems(items []*types.ObjectWithRequiredField) { + p.Items = items + p.require(paginatedResponseFieldItems) +} + +// SetNext sets the Next field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (p *PaginatedResponse) SetNext(next *string) { + p.Next = next + p.require(paginatedResponseFieldNext) +} + +func (p *PaginatedResponse) UnmarshalJSON(data []byte) error { + type unmarshaler PaginatedResponse + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *p = PaginatedResponse(value) + extraProperties, err := internal.ExtractExtraProperties(data, *p) + if err != nil { + return err + } + p.extraProperties = extraProperties + p.rawJSON = json.RawMessage(data) + return nil +} + +func (p *PaginatedResponse) MarshalJSON() ([]byte, error) { + type embed PaginatedResponse + var marshaler = struct { + embed + }{ + embed: embed(*p), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, p.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (p *PaginatedResponse) String() string { + if p == nil { + return "" + } + if len(p.rawJSON) > 0 { + if value, err := internal.StringifyJSON(p.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(p); err == nil { + return value + } + return fmt.Sprintf("%#v", p) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/client.go new file mode 100644 index 000000000000..a4b0fa7c944f --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/client.go @@ -0,0 +1,95 @@ +// Code generated by Fern. DO NOT EDIT. + +package pagination + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + endpoints "github.com/go-deterministic-ordering/fern/endpoints" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// List items with cursor pagination +func (c *Client) ListItems( + ctx context.Context, + request *fern.ListItemsRequest, + opts ...option.RequestOption, +) (*core.Page[*string, *types.ObjectWithRequiredField, *endpoints.PaginatedResponse], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + c.baseURL, + "", + ) + endpointURL := baseURL + "/pagination" + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + headers := internal.MergeHeaders( + c.options.ToHeader(), + options.ToHeader(), + ) + prepareCall := func(pageRequest *core.PageRequest[*string]) *internal.CallParams { + if pageRequest.Cursor != nil { + queryParams.Set("cursor", *pageRequest.Cursor) + } + nextURL := endpointURL + if len(queryParams) > 0 { + nextURL += "?" + queryParams.Encode() + } + return &internal.CallParams{ + URL: nextURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: pageRequest.Response, + } + } + readPageResponse := func(response *endpoints.PaginatedResponse) *core.PageResponse[*string, *types.ObjectWithRequiredField, *endpoints.PaginatedResponse] { + var zeroValue *string + next := response.GetNext() + results := response.GetItems() + return &core.PageResponse[*string, *types.ObjectWithRequiredField, *endpoints.PaginatedResponse]{ + Results: results, + Response: response, + Next: next, + Done: next == zeroValue, + } + } + pager := internal.NewCursorPager( + c.caller, + prepareCall, + readPageResponse, + ) + return pager.GetPage(ctx, request.Cursor) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/endpoints_pagination_test/endpoints_pagination_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/endpoints_pagination_test/endpoints_pagination_test.go new file mode 100644 index 000000000000..5eaddcfa6b05 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/endpoints_pagination_test/endpoints_pagination_test.go @@ -0,0 +1,93 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_pagination_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsPaginationListItemsWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.ListItemsRequest{ + Cursor: fern.String( + "cursor", + ), + Limit: fern.Int( + 1, + ), + } + _, invocationErr := client.Endpoints.Pagination.ListItems( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPaginationListItemsWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPaginationListItemsWithWireMock", "GET", "/pagination", map[string]string{"cursor": "cursor", "limit": "1"}, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/raw_client.go new file mode 100644 index 000000000000..abb64af21c35 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination/raw_client.go @@ -0,0 +1,27 @@ +// Code generated by Fern. DO NOT EDIT. + +package pagination + +import ( + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/pagination_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination_test.go new file mode 100644 index 000000000000..b0cd7afced91 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/pagination_test.go @@ -0,0 +1,236 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints + +import ( + json "encoding/json" + types "github.com/go-deterministic-ordering/fern/types" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" +) + +func TestSettersPaginatedResponse(t *testing.T) { + t.Run("SetItems", func(t *testing.T) { + obj := &PaginatedResponse{} + var fernTestValueItems []*types.ObjectWithRequiredField + obj.SetItems(fernTestValueItems) + assert.Equal(t, fernTestValueItems, obj.Items) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetNext", func(t *testing.T) { + obj := &PaginatedResponse{} + var fernTestValueNext *string + obj.SetNext(fernTestValueNext) + assert.Equal(t, fernTestValueNext, obj.Next) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersPaginatedResponse(t *testing.T) { + t.Run("GetItems", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PaginatedResponse{} + var expected []*types.ObjectWithRequiredField + obj.Items = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetItems(), "getter should return the property value") + }) + + t.Run("GetItems_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PaginatedResponse{} + obj.Items = nil + + // Act & Assert + assert.Nil(t, obj.GetItems(), "getter should return nil when property is nil") + }) + + t.Run("GetItems_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *PaginatedResponse + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetItems() // Should return zero value + }) + + t.Run("GetNext", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PaginatedResponse{} + var expected *string + obj.Next = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetNext(), "getter should return the property value") + }) + + t.Run("GetNext_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PaginatedResponse{} + obj.Next = nil + + // Act & Assert + assert.Nil(t, obj.GetNext(), "getter should return nil when property is nil") + }) + + t.Run("GetNext_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *PaginatedResponse + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetNext() // Should return zero value + }) + +} + +func TestSettersMarkExplicitPaginatedResponse(t *testing.T) { + t.Run("SetItems_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PaginatedResponse{} + var fernTestValueItems []*types.ObjectWithRequiredField + + // Act + obj.SetItems(fernTestValueItems) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetNext_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PaginatedResponse{} + var fernTestValueNext *string + + // Act + obj.SetNext(fernTestValueNext) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestJSONMarshalingPaginatedResponse(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PaginatedResponse{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled PaginatedResponse + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj PaginatedResponse + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj PaginatedResponse + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestStringPaginatedResponse(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &PaginatedResponse{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *PaginatedResponse + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestExtraPropertiesPaginatedResponse(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &PaginatedResponse{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *PaginatedResponse + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/params/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/params/client.go new file mode 100644 index 000000000000..8b422616d297 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/params/client.go @@ -0,0 +1,194 @@ +// Code generated by Fern. DO NOT EDIT. + +package params + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + io "io" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// GET with path param +func (c *Client) GetWithPath( + ctx context.Context, + param string, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.GetWithPath( + ctx, + param, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +// GET with path param +func (c *Client) GetWithInlinePath( + ctx context.Context, + request *fern.GetWithInlinePath, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.GetWithInlinePath( + ctx, + request, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +// GET with query param +func (c *Client) GetWithQuery( + ctx context.Context, + request *fern.GetWithQuery, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.GetWithQuery( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +// GET with multiple of same query param +func (c *Client) GetWithAllowMultipleQuery( + ctx context.Context, + request *fern.GetWithMultipleQuery, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.GetWithAllowMultipleQuery( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +// GET with path and query params +func (c *Client) GetWithPathAndQuery( + ctx context.Context, + param string, + request *fern.GetWithPathAndQuery, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.GetWithPathAndQuery( + ctx, + param, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +// GET with path and query params +func (c *Client) GetWithInlinePathAndQuery( + ctx context.Context, + request *fern.GetWithInlinePathAndQuery, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.GetWithInlinePathAndQuery( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} + +// PUT to update with path param +func (c *Client) ModifyWithPath( + ctx context.Context, + param string, + request string, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.ModifyWithPath( + ctx, + param, + request, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +// PUT to update with path param +func (c *Client) ModifyWithInlinePath( + ctx context.Context, + request *fern.ModifyResourceAtInlinedPath, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.ModifyWithInlinePath( + ctx, + request, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +// POST bytes with path param returning object +func (c *Client) UploadWithPath( + ctx context.Context, + param string, + request io.Reader, + opts ...option.RequestOption, +) (*types.ObjectWithRequiredField, error) { + response, err := c.WithRawResponse.UploadWithPath( + ctx, + param, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/params/endpoints_params_test/endpoints_params_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/params/endpoints_params_test/endpoints_params_test.go new file mode 100644 index 000000000000..070cafd67f5c --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/params/endpoints_params_test/endpoints_params_test.go @@ -0,0 +1,259 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_params_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsParamsGetWithPathWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.Params.GetWithPath( + context.TODO(), + "param", + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsGetWithPathWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsGetWithPathWithWireMock", "GET", "/params/path/param", nil, 1) +} + +func TestEndpointsParamsGetWithPathWithWireMock2( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.Params.GetWithPath( + context.TODO(), + "param", + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsGetWithPathWithWireMock2"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsGetWithPathWithWireMock2", "GET", "/params/path/param", nil, 1) +} + +func TestEndpointsParamsGetWithQueryWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.GetWithQuery{ + Query: "query", + Number: 1, + } + invocationErr := client.Endpoints.Params.GetWithQuery( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsGetWithQueryWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsGetWithQueryWithWireMock", "GET", "/params", map[string]string{"query": "query", "number": "1"}, 1) +} + +func TestEndpointsParamsGetWithQueryWithWireMock2( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.GetWithQuery{ + Query: "query", + Number: 1, + } + invocationErr := client.Endpoints.Params.GetWithQuery( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsGetWithQueryWithWireMock2"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsGetWithQueryWithWireMock2", "GET", "/params", map[string]string{"query": "query", "number": "1"}, 1) +} + +func TestEndpointsParamsGetWithPathAndQueryWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.GetWithPathAndQuery{ + Query: "query", + } + invocationErr := client.Endpoints.Params.GetWithPathAndQuery( + context.TODO(), + "param", + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsGetWithPathAndQueryWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsGetWithPathAndQueryWithWireMock", "GET", "/params/path-query/param", map[string]string{"query": "query"}, 1) +} + +func TestEndpointsParamsGetWithPathAndQueryWithWireMock2( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.GetWithPathAndQuery{ + Query: "query", + } + invocationErr := client.Endpoints.Params.GetWithPathAndQuery( + context.TODO(), + "param", + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsGetWithPathAndQueryWithWireMock2"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsGetWithPathAndQueryWithWireMock2", "GET", "/params/path-query/param", map[string]string{"query": "query"}, 1) +} + +func TestEndpointsParamsModifyWithPathWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := "string" + _, invocationErr := client.Endpoints.Params.ModifyWithPath( + context.TODO(), + "param", + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsModifyWithPathWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsModifyWithPathWithWireMock", "PUT", "/params/path/param", nil, 1) +} + +func TestEndpointsParamsModifyWithPathWithWireMock2( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := "string" + _, invocationErr := client.Endpoints.Params.ModifyWithPath( + context.TODO(), + "param", + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsParamsModifyWithPathWithWireMock2"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsParamsModifyWithPathWithWireMock2", "PUT", "/params/path/param", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/params/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/params/raw_client.go new file mode 100644 index 000000000000..53cd8dd91eaf --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/params/raw_client.go @@ -0,0 +1,440 @@ +// Code generated by Fern. DO NOT EDIT. + +package params + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + io "io" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetWithPath( + ctx context.Context, + param string, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/params/path/%v", + param, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetWithInlinePath( + ctx context.Context, + request *fern.GetWithInlinePath, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/params/path/%v", + request.Param, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetWithQuery( + ctx context.Context, + request *fern.GetWithQuery, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/params" + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) GetWithAllowMultipleQuery( + ctx context.Context, + request *fern.GetWithMultipleQuery, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/params" + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) GetWithPathAndQuery( + ctx context.Context, + param string, + request *fern.GetWithPathAndQuery, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/params/path-query/%v", + param, + ) + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) GetWithInlinePathAndQuery( + ctx context.Context, + request *fern.GetWithInlinePathAndQuery, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/params/path-query/%v", + request.Param, + ) + queryParams, err := internal.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} + +func (r *RawClient) ModifyWithPath( + ctx context.Context, + param string, + request string, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/params/path/%v", + param, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPut, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) ModifyWithInlinePath( + ctx context.Context, + request *fern.ModifyResourceAtInlinedPath, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/params/path/%v", + request.Param, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPut, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) UploadWithPath( + ctx context.Context, + param string, + request io.Reader, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithRequiredField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/params/path/%v", + param, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithRequiredField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithRequiredField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/client.go new file mode 100644 index 000000000000..71f9624202ef --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/client.go @@ -0,0 +1,178 @@ +// Code generated by Fern. DO NOT EDIT. + +package primitive + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + uuid "github.com/google/uuid" + time "time" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) GetAndReturnString( + ctx context.Context, + request string, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.GetAndReturnString( + ctx, + request, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnInt( + ctx context.Context, + request int, + opts ...option.RequestOption, +) (int, error) { + response, err := c.WithRawResponse.GetAndReturnInt( + ctx, + request, + opts..., + ) + if err != nil { + return 0, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnLong( + ctx context.Context, + request int64, + opts ...option.RequestOption, +) (int64, error) { + response, err := c.WithRawResponse.GetAndReturnLong( + ctx, + request, + opts..., + ) + if err != nil { + return int64(0), err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnDouble( + ctx context.Context, + request float64, + opts ...option.RequestOption, +) (float64, error) { + response, err := c.WithRawResponse.GetAndReturnDouble( + ctx, + request, + opts..., + ) + if err != nil { + return 0, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnBool( + ctx context.Context, + request bool, + opts ...option.RequestOption, +) (bool, error) { + response, err := c.WithRawResponse.GetAndReturnBool( + ctx, + request, + opts..., + ) + if err != nil { + return false, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnDatetime( + ctx context.Context, + request time.Time, + opts ...option.RequestOption, +) (time.Time, error) { + response, err := c.WithRawResponse.GetAndReturnDatetime( + ctx, + request, + opts..., + ) + if err != nil { + return time.Time{}, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnDate( + ctx context.Context, + request time.Time, + opts ...option.RequestOption, +) (time.Time, error) { + response, err := c.WithRawResponse.GetAndReturnDate( + ctx, + request, + opts..., + ) + if err != nil { + return time.Time{}, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnUuid( + ctx context.Context, + request uuid.UUID, + opts ...option.RequestOption, +) (uuid.UUID, error) { + response, err := c.WithRawResponse.GetAndReturnUuid( + ctx, + request, + opts..., + ) + if err != nil { + return uuid.UUID{}, err + } + return response.Body, nil +} + +func (c *Client) GetAndReturnBase64( + ctx context.Context, + request []byte, + opts ...option.RequestOption, +) ([]byte, error) { + response, err := c.WithRawResponse.GetAndReturnBase64( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/endpoints_primitive_test/endpoints_primitive_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/endpoints_primitive_test/endpoints_primitive_test.go new file mode 100644 index 000000000000..c669ac3b5585 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/endpoints_primitive_test/endpoints_primitive_test.go @@ -0,0 +1,252 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_primitive_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + uuid "github.com/google/uuid" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsPrimitiveGetAndReturnStringWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := "string" + _, invocationErr := client.Endpoints.Primitive.GetAndReturnString( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnStringWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnStringWithWireMock", "POST", "/primitive/string", nil, 1) +} + +func TestEndpointsPrimitiveGetAndReturnIntWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := 1 + _, invocationErr := client.Endpoints.Primitive.GetAndReturnInt( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnIntWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnIntWithWireMock", "POST", "/primitive/integer", nil, 1) +} + +func TestEndpointsPrimitiveGetAndReturnLongWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := int64(1000000) + _, invocationErr := client.Endpoints.Primitive.GetAndReturnLong( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnLongWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnLongWithWireMock", "POST", "/primitive/long", nil, 1) +} + +func TestEndpointsPrimitiveGetAndReturnDoubleWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := 1.1 + _, invocationErr := client.Endpoints.Primitive.GetAndReturnDouble( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnDoubleWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnDoubleWithWireMock", "POST", "/primitive/double", nil, 1) +} + +func TestEndpointsPrimitiveGetAndReturnBoolWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := true + _, invocationErr := client.Endpoints.Primitive.GetAndReturnBool( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnBoolWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnBoolWithWireMock", "POST", "/primitive/boolean", nil, 1) +} + +func TestEndpointsPrimitiveGetAndReturnDatetimeWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ) + _, invocationErr := client.Endpoints.Primitive.GetAndReturnDatetime( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnDatetimeWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnDatetimeWithWireMock", "POST", "/primitive/datetime", nil, 1) +} + +func TestEndpointsPrimitiveGetAndReturnUuidWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ) + _, invocationErr := client.Endpoints.Primitive.GetAndReturnUuid( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnUuidWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnUuidWithWireMock", "POST", "/primitive/uuid", nil, 1) +} + +func TestEndpointsPrimitiveGetAndReturnBase64WithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := []byte("SGVsbG8gd29ybGQh") + _, invocationErr := client.Endpoints.Primitive.GetAndReturnBase64( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPrimitiveGetAndReturnBase64WithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPrimitiveGetAndReturnBase64WithWireMock", "POST", "/primitive/base64", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/raw_client.go new file mode 100644 index 000000000000..6476d63c855b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/primitive/raw_client.go @@ -0,0 +1,401 @@ +// Code generated by Fern. DO NOT EDIT. + +package primitive + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + uuid "github.com/google/uuid" + http "net/http" + time "time" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetAndReturnString( + ctx context.Context, + request string, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/string" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnInt( + ctx context.Context, + request int, + opts ...option.RequestOption, +) (*core.Response[int], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/integer" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response int + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[int]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnLong( + ctx context.Context, + request int64, + opts ...option.RequestOption, +) (*core.Response[int64], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/long" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response int64 + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[int64]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnDouble( + ctx context.Context, + request float64, + opts ...option.RequestOption, +) (*core.Response[float64], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/double" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response float64 + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[float64]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnBool( + ctx context.Context, + request bool, + opts ...option.RequestOption, +) (*core.Response[bool], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/boolean" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response bool + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[bool]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnDatetime( + ctx context.Context, + request time.Time, + opts ...option.RequestOption, +) (*core.Response[time.Time], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/datetime" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response time.Time + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[time.Time]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnDate( + ctx context.Context, + request time.Time, + opts ...option.RequestOption, +) (*core.Response[time.Time], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/date" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response time.Time + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[time.Time]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnUuid( + ctx context.Context, + request uuid.UUID, + opts ...option.RequestOption, +) (*core.Response[uuid.UUID], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/uuid" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response uuid.UUID + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[uuid.UUID]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) GetAndReturnBase64( + ctx context.Context, + request []byte, + opts ...option.RequestOption, +) (*core.Response[[]byte], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/primitive/base64" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response []byte + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[[]byte]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/put.go b/seed/go-sdk/go-deterministic-ordering/endpoints/put.go new file mode 100644 index 000000000000..a61bf604657a --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/put.go @@ -0,0 +1,300 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + big "math/big" +) + +var ( + errorFieldCategory = big.NewInt(1 << 0) + errorFieldCode = big.NewInt(1 << 1) + errorFieldDetail = big.NewInt(1 << 2) + errorFieldField = big.NewInt(1 << 3) +) + +type Error struct { + Category ErrorCategory `json:"category" url:"category"` + Code ErrorCode `json:"code" url:"code"` + Detail *string `json:"detail,omitempty" url:"detail,omitempty"` + Field *string `json:"field,omitempty" url:"field,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (e *Error) GetCategory() ErrorCategory { + if e == nil { + return "" + } + return e.Category +} + +func (e *Error) GetCode() ErrorCode { + if e == nil { + return "" + } + return e.Code +} + +func (e *Error) GetDetail() *string { + if e == nil { + return nil + } + return e.Detail +} + +func (e *Error) GetField() *string { + if e == nil { + return nil + } + return e.Field +} + +func (e *Error) GetExtraProperties() map[string]interface{} { + if e == nil { + return nil + } + return e.extraProperties +} + +func (e *Error) require(field *big.Int) { + if e.explicitFields == nil { + e.explicitFields = big.NewInt(0) + } + e.explicitFields.Or(e.explicitFields, field) +} + +// SetCategory sets the Category field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (e *Error) SetCategory(category ErrorCategory) { + e.Category = category + e.require(errorFieldCategory) +} + +// SetCode sets the Code field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (e *Error) SetCode(code ErrorCode) { + e.Code = code + e.require(errorFieldCode) +} + +// SetDetail sets the Detail field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (e *Error) SetDetail(detail *string) { + e.Detail = detail + e.require(errorFieldDetail) +} + +// SetField sets the Field field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (e *Error) SetField(field *string) { + e.Field = field + e.require(errorFieldField) +} + +func (e *Error) UnmarshalJSON(data []byte) error { + type unmarshaler Error + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *e = Error(value) + extraProperties, err := internal.ExtractExtraProperties(data, *e) + if err != nil { + return err + } + e.extraProperties = extraProperties + e.rawJSON = json.RawMessage(data) + return nil +} + +func (e *Error) MarshalJSON() ([]byte, error) { + type embed Error + var marshaler = struct { + embed + }{ + embed: embed(*e), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, e.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (e *Error) String() string { + if e == nil { + return "" + } + if len(e.rawJSON) > 0 { + if value, err := internal.StringifyJSON(e.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} + +type ErrorCategory string + +const ( + ErrorCategoryApiError ErrorCategory = "API_ERROR" + ErrorCategoryAuthenticationError ErrorCategory = "AUTHENTICATION_ERROR" + ErrorCategoryInvalidRequestError ErrorCategory = "INVALID_REQUEST_ERROR" +) + +func NewErrorCategoryFromString(s string) (ErrorCategory, error) { + switch s { + case "API_ERROR": + return ErrorCategoryApiError, nil + case "AUTHENTICATION_ERROR": + return ErrorCategoryAuthenticationError, nil + case "INVALID_REQUEST_ERROR": + return ErrorCategoryInvalidRequestError, nil + } + var t ErrorCategory + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (e ErrorCategory) Ptr() *ErrorCategory { + return &e +} + +type ErrorCode string + +const ( + ErrorCodeInternalServerError ErrorCode = "INTERNAL_SERVER_ERROR" + ErrorCodeUnauthorized ErrorCode = "UNAUTHORIZED" + ErrorCodeForbidden ErrorCode = "FORBIDDEN" + ErrorCodeBadRequest ErrorCode = "BAD_REQUEST" + ErrorCodeConflict ErrorCode = "CONFLICT" + ErrorCodeGone ErrorCode = "GONE" + ErrorCodeUnprocessableEntity ErrorCode = "UNPROCESSABLE_ENTITY" + ErrorCodeNotImplemented ErrorCode = "NOT_IMPLEMENTED" + ErrorCodeBadGateway ErrorCode = "BAD_GATEWAY" + ErrorCodeServiceUnavailable ErrorCode = "SERVICE_UNAVAILABLE" + ErrorCodeUnknown ErrorCode = "Unknown" +) + +func NewErrorCodeFromString(s string) (ErrorCode, error) { + switch s { + case "INTERNAL_SERVER_ERROR": + return ErrorCodeInternalServerError, nil + case "UNAUTHORIZED": + return ErrorCodeUnauthorized, nil + case "FORBIDDEN": + return ErrorCodeForbidden, nil + case "BAD_REQUEST": + return ErrorCodeBadRequest, nil + case "CONFLICT": + return ErrorCodeConflict, nil + case "GONE": + return ErrorCodeGone, nil + case "UNPROCESSABLE_ENTITY": + return ErrorCodeUnprocessableEntity, nil + case "NOT_IMPLEMENTED": + return ErrorCodeNotImplemented, nil + case "BAD_GATEWAY": + return ErrorCodeBadGateway, nil + case "SERVICE_UNAVAILABLE": + return ErrorCodeServiceUnavailable, nil + case "Unknown": + return ErrorCodeUnknown, nil + } + var t ErrorCode + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (e ErrorCode) Ptr() *ErrorCode { + return &e +} + +var ( + putResponseFieldErrors = big.NewInt(1 << 0) +) + +type PutResponse struct { + Errors []*Error `json:"errors,omitempty" url:"errors,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (p *PutResponse) GetErrors() []*Error { + if p == nil { + return nil + } + return p.Errors +} + +func (p *PutResponse) GetExtraProperties() map[string]interface{} { + if p == nil { + return nil + } + return p.extraProperties +} + +func (p *PutResponse) require(field *big.Int) { + if p.explicitFields == nil { + p.explicitFields = big.NewInt(0) + } + p.explicitFields.Or(p.explicitFields, field) +} + +// SetErrors sets the Errors field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (p *PutResponse) SetErrors(errors []*Error) { + p.Errors = errors + p.require(putResponseFieldErrors) +} + +func (p *PutResponse) UnmarshalJSON(data []byte) error { + type unmarshaler PutResponse + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *p = PutResponse(value) + extraProperties, err := internal.ExtractExtraProperties(data, *p) + if err != nil { + return err + } + p.extraProperties = extraProperties + p.rawJSON = json.RawMessage(data) + return nil +} + +func (p *PutResponse) MarshalJSON() ([]byte, error) { + type embed PutResponse + var marshaler = struct { + embed + }{ + embed: embed(*p), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, p.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (p *PutResponse) String() string { + if p == nil { + return "" + } + if len(p.rawJSON) > 0 { + if value, err := internal.StringifyJSON(p.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(p); err == nil { + return value + } + return fmt.Sprintf("%#v", p) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/put/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/put/client.go new file mode 100644 index 000000000000..df364ae6dbc0 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/put/client.go @@ -0,0 +1,50 @@ +// Code generated by Fern. DO NOT EDIT. + +package put + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + endpoints "github.com/go-deterministic-ordering/fern/endpoints" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) Add( + ctx context.Context, + request *fern.PutRequest, + opts ...option.RequestOption, +) (*endpoints.PutResponse, error) { + response, err := c.WithRawResponse.Add( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/put/endpoints_put_test/endpoints_put_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/put/endpoints_put_test/endpoints_put_test.go new file mode 100644 index 000000000000..06750dfd7f15 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/put/endpoints_put_test/endpoints_put_test.go @@ -0,0 +1,88 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_put_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsPutAddWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.PutRequest{ + Id: "id", + } + _, invocationErr := client.Endpoints.Put.Add( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsPutAddWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsPutAddWithWireMock", "PUT", "/id", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/put/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/put/raw_client.go new file mode 100644 index 000000000000..d2c1e8cd9c72 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/put/raw_client.go @@ -0,0 +1,75 @@ +// Code generated by Fern. DO NOT EDIT. + +package put + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + endpoints "github.com/go-deterministic-ordering/fern/endpoints" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) Add( + ctx context.Context, + request *fern.PutRequest, + opts ...option.RequestOption, +) (*core.Response[*endpoints.PutResponse], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := internal.EncodeURL( + baseURL+"/%v", + request.Id, + ) + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *endpoints.PutResponse + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPut, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*endpoints.PutResponse]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/put_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/put_test.go new file mode 100644 index 000000000000..15fe369125ae --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/put_test.go @@ -0,0 +1,640 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints + +import ( + json "encoding/json" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" +) + +func TestSettersError(t *testing.T) { + t.Run("SetCategory", func(t *testing.T) { + obj := &Error{} + var fernTestValueCategory ErrorCategory + obj.SetCategory(fernTestValueCategory) + assert.Equal(t, fernTestValueCategory, obj.Category) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetCode", func(t *testing.T) { + obj := &Error{} + var fernTestValueCode ErrorCode + obj.SetCode(fernTestValueCode) + assert.Equal(t, fernTestValueCode, obj.Code) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetDetail", func(t *testing.T) { + obj := &Error{} + var fernTestValueDetail *string + obj.SetDetail(fernTestValueDetail) + assert.Equal(t, fernTestValueDetail, obj.Detail) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetField", func(t *testing.T) { + obj := &Error{} + var fernTestValueField *string + obj.SetField(fernTestValueField) + assert.Equal(t, fernTestValueField, obj.Field) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersError(t *testing.T) { + t.Run("GetCategory", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var expected ErrorCategory + obj.Category = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetCategory(), "getter should return the property value") + }) + + t.Run("GetCategory_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Error + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetCategory() // Should return zero value + }) + + t.Run("GetCode", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var expected ErrorCode + obj.Code = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetCode(), "getter should return the property value") + }) + + t.Run("GetCode_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Error + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetCode() // Should return zero value + }) + + t.Run("GetDetail", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var expected *string + obj.Detail = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetDetail(), "getter should return the property value") + }) + + t.Run("GetDetail_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + obj.Detail = nil + + // Act & Assert + assert.Nil(t, obj.GetDetail(), "getter should return nil when property is nil") + }) + + t.Run("GetDetail_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Error + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetDetail() // Should return zero value + }) + + t.Run("GetField", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var expected *string + obj.Field = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetField(), "getter should return the property value") + }) + + t.Run("GetField_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + obj.Field = nil + + // Act & Assert + assert.Nil(t, obj.GetField(), "getter should return nil when property is nil") + }) + + t.Run("GetField_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Error + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetField() // Should return zero value + }) + +} + +func TestSettersMarkExplicitError(t *testing.T) { + t.Run("SetCategory_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var fernTestValueCategory ErrorCategory + + // Act + obj.SetCategory(fernTestValueCategory) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetCode_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var fernTestValueCode ErrorCode + + // Act + obj.SetCode(fernTestValueCode) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetDetail_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var fernTestValueDetail *string + + // Act + obj.SetDetail(fernTestValueDetail) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetField_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + var fernTestValueField *string + + // Act + obj.SetField(fernTestValueField) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersPutResponse(t *testing.T) { + t.Run("SetErrors", func(t *testing.T) { + obj := &PutResponse{} + var fernTestValueErrors []*Error + obj.SetErrors(fernTestValueErrors) + assert.Equal(t, fernTestValueErrors, obj.Errors) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersPutResponse(t *testing.T) { + t.Run("GetErrors", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PutResponse{} + var expected []*Error + obj.Errors = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetErrors(), "getter should return the property value") + }) + + t.Run("GetErrors_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PutResponse{} + obj.Errors = nil + + // Act & Assert + assert.Nil(t, obj.GetErrors(), "getter should return nil when property is nil") + }) + + t.Run("GetErrors_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *PutResponse + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetErrors() // Should return zero value + }) + +} + +func TestSettersMarkExplicitPutResponse(t *testing.T) { + t.Run("SetErrors_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PutResponse{} + var fernTestValueErrors []*Error + + // Act + obj.SetErrors(fernTestValueErrors) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestJSONMarshalingError(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Error{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled Error + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj Error + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj Error + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingPutResponse(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PutResponse{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled PutResponse + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj PutResponse + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj PutResponse + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestStringError(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &Error{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Error + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringPutResponse(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &PutResponse{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *PutResponse + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestEnumErrorCategory(t *testing.T) { + t.Run("NewFromString_API_ERROR", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCategoryFromString("API_ERROR") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCategory("API_ERROR"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_AUTHENTICATION_ERROR", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCategoryFromString("AUTHENTICATION_ERROR") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCategory("AUTHENTICATION_ERROR"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_INVALID_REQUEST_ERROR", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCategoryFromString("INVALID_REQUEST_ERROR") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCategory("INVALID_REQUEST_ERROR"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_Invalid", func(t *testing.T) { + _, err := NewErrorCategoryFromString("invalid_value_that_does_not_exist") + assert.Error(t, err) + }) + + t.Run("Ptr", func(t *testing.T) { + val, err := NewErrorCategoryFromString("API_ERROR") + assert.NoError(t, err) + ptr := val.Ptr() + assert.NotNil(t, ptr) + assert.Equal(t, val, *ptr) + }) +} + +func TestEnumErrorCode(t *testing.T) { + t.Run("NewFromString_INTERNAL_SERVER_ERROR", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("INTERNAL_SERVER_ERROR") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("INTERNAL_SERVER_ERROR"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_UNAUTHORIZED", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("UNAUTHORIZED") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("UNAUTHORIZED"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_FORBIDDEN", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("FORBIDDEN") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("FORBIDDEN"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_BAD_REQUEST", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("BAD_REQUEST") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("BAD_REQUEST"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_CONFLICT", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("CONFLICT") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("CONFLICT"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_GONE", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("GONE") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("GONE"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_UNPROCESSABLE_ENTITY", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("UNPROCESSABLE_ENTITY") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("UNPROCESSABLE_ENTITY"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_NOT_IMPLEMENTED", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("NOT_IMPLEMENTED") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("NOT_IMPLEMENTED"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_BAD_GATEWAY", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("BAD_GATEWAY") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("BAD_GATEWAY"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_SERVICE_UNAVAILABLE", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("SERVICE_UNAVAILABLE") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("SERVICE_UNAVAILABLE"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_Unknown", func(t *testing.T) { + t.Parallel() + val, err := NewErrorCodeFromString("Unknown") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, ErrorCode("Unknown"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_Invalid", func(t *testing.T) { + _, err := NewErrorCodeFromString("invalid_value_that_does_not_exist") + assert.Error(t, err) + }) + + t.Run("Ptr", func(t *testing.T) { + val, err := NewErrorCodeFromString("INTERNAL_SERVER_ERROR") + assert.NoError(t, err) + ptr := val.Ptr() + assert.NotNil(t, ptr) + assert.Equal(t, val, *ptr) + }) +} + +func TestExtraPropertiesError(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &Error{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Error + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesPutResponse(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &PutResponse{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *PutResponse + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/union/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/union/client.go new file mode 100644 index 000000000000..f88f77c8fa62 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/union/client.go @@ -0,0 +1,49 @@ +// Code generated by Fern. DO NOT EDIT. + +package union + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) GetAndReturnUnion( + ctx context.Context, + request *types.Animal, + opts ...option.RequestOption, +) (*types.Animal, error) { + response, err := c.WithRawResponse.GetAndReturnUnion( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/union/endpoints_union_test/endpoints_union_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/union/endpoints_union_test/endpoints_union_test.go new file mode 100644 index 000000000000..be1a1ded2b76 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/union/endpoints_union_test/endpoints_union_test.go @@ -0,0 +1,91 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_union_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsUnionGetAndReturnUnionWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &types.Animal{ + Dog: &types.Dog{ + Name: "name", + LikesToWoof: true, + }, + } + _, invocationErr := client.Endpoints.Union.GetAndReturnUnion( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsUnionGetAndReturnUnionWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsUnionGetAndReturnUnionWithWireMock", "POST", "/union", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/union/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/union/raw_client.go new file mode 100644 index 000000000000..356e9081e29b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/union/raw_client.go @@ -0,0 +1,72 @@ +// Code generated by Fern. DO NOT EDIT. + +package union + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetAndReturnUnion( + ctx context.Context, + request *types.Animal, + opts ...option.RequestOption, +) (*core.Response[*types.Animal], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/union" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.Animal + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.Animal]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/urls/client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/urls/client.go new file mode 100644 index 000000000000..3e6a71cbade2 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/urls/client.go @@ -0,0 +1,88 @@ +// Code generated by Fern. DO NOT EDIT. + +package urls + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) WithMixedCase( + ctx context.Context, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.WithMixedCase( + ctx, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +func (c *Client) NoEndingSlash( + ctx context.Context, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.NoEndingSlash( + ctx, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +func (c *Client) WithEndingSlash( + ctx context.Context, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.WithEndingSlash( + ctx, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} + +func (c *Client) WithUnderscores( + ctx context.Context, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.WithUnderscores( + ctx, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/urls/endpoints_urls_test/endpoints_urls_test.go b/seed/go-sdk/go-deterministic-ordering/endpoints/urls/endpoints_urls_test/endpoints_urls_test.go new file mode 100644 index 000000000000..68ecca980220 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/urls/endpoints_urls_test/endpoints_urls_test.go @@ -0,0 +1,146 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints_urls_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestEndpointsUrlsWithMixedCaseWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.Urls.WithMixedCase( + context.TODO(), + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsUrlsWithMixedCaseWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsUrlsWithMixedCaseWithWireMock", "GET", "/urls/MixedCase", nil, 1) +} + +func TestEndpointsUrlsNoEndingSlashWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.Urls.NoEndingSlash( + context.TODO(), + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsUrlsNoEndingSlashWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsUrlsNoEndingSlashWithWireMock", "GET", "/urls/no-ending-slash", nil, 1) +} + +func TestEndpointsUrlsWithEndingSlashWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.Urls.WithEndingSlash( + context.TODO(), + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsUrlsWithEndingSlashWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsUrlsWithEndingSlashWithWireMock", "GET", "/urls/with-ending-slash/", nil, 1) +} + +func TestEndpointsUrlsWithUnderscoresWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.Endpoints.Urls.WithUnderscores( + context.TODO(), + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestEndpointsUrlsWithUnderscoresWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestEndpointsUrlsWithUnderscoresWithWireMock", "GET", "/urls/with_underscores", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/endpoints/urls/raw_client.go b/seed/go-sdk/go-deterministic-ordering/endpoints/urls/raw_client.go new file mode 100644 index 000000000000..678aae2580c7 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/endpoints/urls/raw_client.go @@ -0,0 +1,186 @@ +// Code generated by Fern. DO NOT EDIT. + +package urls + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) WithMixedCase( + ctx context.Context, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/urls/MixedCase" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) NoEndingSlash( + ctx context.Context, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/urls/no-ending-slash" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) WithEndingSlash( + ctx context.Context, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/urls/with-ending-slash/" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) WithUnderscores( + ctx context.Context, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/urls/with_underscores" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/error_codes.go b/seed/go-sdk/go-deterministic-ordering/error_codes.go new file mode 100644 index 000000000000..e9481e90064b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/error_codes.go @@ -0,0 +1,16 @@ +// Code generated by Fern. DO NOT EDIT. + +package exhaustive + +import ( + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" +) + +var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{ + 400: func(apiError *core.APIError) error { + return &BadRequestBody{ + APIError: apiError, + } + }, +} diff --git a/seed/go-sdk/go-deterministic-ordering/errors.go b/seed/go-sdk/go-deterministic-ordering/errors.go new file mode 100644 index 000000000000..10f6766397f0 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/errors.go @@ -0,0 +1,31 @@ +// Code generated by Fern. DO NOT EDIT. + +package exhaustive + +import ( + json "encoding/json" + core "github.com/go-deterministic-ordering/fern/core" +) + +type BadRequestBody struct { + *core.APIError + Body *BadObjectRequestInfo +} + +func (b *BadRequestBody) UnmarshalJSON(data []byte) error { + var body *BadObjectRequestInfo + if err := json.Unmarshal(data, &body); err != nil { + return err + } + b.StatusCode = 400 + b.Body = body + return nil +} + +func (b *BadRequestBody) MarshalJSON() ([]byte, error) { + return json.Marshal(b.Body) +} + +func (b *BadRequestBody) Unwrap() error { + return b.APIError +} diff --git a/seed/go-sdk/go-deterministic-ordering/file_param.go b/seed/go-sdk/go-deterministic-ordering/file_param.go new file mode 100644 index 000000000000..8cc3a5619d02 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/file_param.go @@ -0,0 +1,41 @@ +package exhaustive + +import ( + "io" +) + +// FileParam is a file type suitable for multipart/form-data uploads. +type FileParam struct { + io.Reader + filename string + contentType string +} + +// FileParamOption adapts the behavior of the FileParam. No options are +// implemented yet, but this interface allows for future extensibility. +type FileParamOption interface { + apply() +} + +// NewFileParam returns a *FileParam type suitable for multipart/form-data uploads. All file +// upload endpoints accept a simple io.Reader, which is usually created by opening a file +// via os.Open. +// +// However, some endpoints require additional metadata about the file such as a specific +// Content-Type or custom filename. FileParam makes it easier to create the correct type +// signature for these endpoints. +func NewFileParam( + reader io.Reader, + filename string, + contentType string, + opts ...FileParamOption, +) *FileParam { + return &FileParam{ + Reader: reader, + filename: filename, + contentType: contentType, + } +} + +func (f *FileParam) Name() string { return f.filename } +func (f *FileParam) ContentType() string { return f.contentType } diff --git a/seed/go-sdk/go-deterministic-ordering/general_errors.go b/seed/go-sdk/go-deterministic-ordering/general_errors.go new file mode 100644 index 000000000000..ad42611806ad --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/general_errors.go @@ -0,0 +1,94 @@ +// Code generated by Fern. DO NOT EDIT. + +package exhaustive + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + big "math/big" +) + +var ( + badObjectRequestInfoFieldMessage = big.NewInt(1 << 0) +) + +type BadObjectRequestInfo struct { + Message string `json:"message" url:"message"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (b *BadObjectRequestInfo) GetMessage() string { + if b == nil { + return "" + } + return b.Message +} + +func (b *BadObjectRequestInfo) GetExtraProperties() map[string]interface{} { + if b == nil { + return nil + } + return b.extraProperties +} + +func (b *BadObjectRequestInfo) require(field *big.Int) { + if b.explicitFields == nil { + b.explicitFields = big.NewInt(0) + } + b.explicitFields.Or(b.explicitFields, field) +} + +// SetMessage sets the Message field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (b *BadObjectRequestInfo) SetMessage(message string) { + b.Message = message + b.require(badObjectRequestInfoFieldMessage) +} + +func (b *BadObjectRequestInfo) UnmarshalJSON(data []byte) error { + type unmarshaler BadObjectRequestInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *b = BadObjectRequestInfo(value) + extraProperties, err := internal.ExtractExtraProperties(data, *b) + if err != nil { + return err + } + b.extraProperties = extraProperties + b.rawJSON = json.RawMessage(data) + return nil +} + +func (b *BadObjectRequestInfo) MarshalJSON() ([]byte, error) { + type embed BadObjectRequestInfo + var marshaler = struct { + embed + }{ + embed: embed(*b), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, b.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (b *BadObjectRequestInfo) String() string { + if b == nil { + return "" + } + if len(b.rawJSON) > 0 { + if value, err := internal.StringifyJSON(b.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(b); err == nil { + return value + } + return fmt.Sprintf("%#v", b) +} diff --git a/seed/go-sdk/go-deterministic-ordering/general_errors_test.go b/seed/go-sdk/go-deterministic-ordering/general_errors_test.go new file mode 100644 index 000000000000..1cecdfe3535b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/general_errors_test.go @@ -0,0 +1,153 @@ +// Code generated by Fern. DO NOT EDIT. + +package exhaustive + +import ( + json "encoding/json" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" +) + +func TestSettersBadObjectRequestInfo(t *testing.T) { + t.Run("SetMessage", func(t *testing.T) { + obj := &BadObjectRequestInfo{} + var fernTestValueMessage string + obj.SetMessage(fernTestValueMessage) + assert.Equal(t, fernTestValueMessage, obj.Message) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersBadObjectRequestInfo(t *testing.T) { + t.Run("GetMessage", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &BadObjectRequestInfo{} + var expected string + obj.Message = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetMessage(), "getter should return the property value") + }) + + t.Run("GetMessage_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *BadObjectRequestInfo + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetMessage() // Should return zero value + }) + +} + +func TestSettersMarkExplicitBadObjectRequestInfo(t *testing.T) { + t.Run("SetMessage_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &BadObjectRequestInfo{} + var fernTestValueMessage string + + // Act + obj.SetMessage(fernTestValueMessage) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestJSONMarshalingBadObjectRequestInfo(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &BadObjectRequestInfo{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled BadObjectRequestInfo + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj BadObjectRequestInfo + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj BadObjectRequestInfo + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestStringBadObjectRequestInfo(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &BadObjectRequestInfo{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *BadObjectRequestInfo + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestExtraPropertiesBadObjectRequestInfo(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &BadObjectRequestInfo{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *BadObjectRequestInfo + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/go.mod b/seed/go-sdk/go-deterministic-ordering/go.mod new file mode 100644 index 000000000000..3e6c610a55fb --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/go.mod @@ -0,0 +1,16 @@ +module github.com/go-deterministic-ordering/fern + +go 1.21 + +toolchain go1.23.8 + +require github.com/google/uuid v1.6.0 + +require github.com/stretchr/testify v1.8.4 + +require gopkg.in/yaml.v3 v3.0.1 // indirect + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/seed/go-sdk/go-deterministic-ordering/go.sum b/seed/go-sdk/go-deterministic-ordering/go.sum new file mode 100644 index 000000000000..fcca6d128057 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seed/go-sdk/go-deterministic-ordering/inlinedrequests/client.go b/seed/go-sdk/go-deterministic-ordering/inlinedrequests/client.go new file mode 100644 index 000000000000..903a23d78b90 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/inlinedrequests/client.go @@ -0,0 +1,51 @@ +// Code generated by Fern. DO NOT EDIT. + +package inlinedrequests + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// POST with custom object in request body, response is an object +func (c *Client) PostWithObjectBodyandResponse( + ctx context.Context, + request *fern.PostWithObjectBody, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.PostWithObjectBodyandResponse( + ctx, + request, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/inlinedrequests/inlined_requests_test/inlined_requests_test.go b/seed/go-sdk/go-deterministic-ordering/inlinedrequests/inlined_requests_test/inlined_requests_test.go new file mode 100644 index 000000000000..8754b6c05d59 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/inlinedrequests/inlined_requests_test/inlined_requests_test.go @@ -0,0 +1,139 @@ +// Code generated by Fern. DO NOT EDIT. + +package inlined_requests_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + uuid "github.com/google/uuid" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestInlinedRequestsPostWithObjectBodyandResponseWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.PostWithObjectBody{ + FieldString: "string", + Integer: 1, + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } + _, invocationErr := client.InlinedRequests.PostWithObjectBodyandResponse( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestInlinedRequestsPostWithObjectBodyandResponseWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestInlinedRequestsPostWithObjectBodyandResponseWithWireMock", "POST", "/req-bodies/object", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/inlinedrequests/raw_client.go b/seed/go-sdk/go-deterministic-ordering/inlinedrequests/raw_client.go new file mode 100644 index 000000000000..c876cfde3fdf --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/inlinedrequests/raw_client.go @@ -0,0 +1,74 @@ +// Code generated by Fern. DO NOT EDIT. + +package inlinedrequests + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) PostWithObjectBodyandResponse( + ctx context.Context, + request *fern.PostWithObjectBody, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/req-bodies/object" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + ErrorDecoder: internal.NewErrorDecoder(fern.ErrorCodes), + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/caller.go b/seed/go-sdk/go-deterministic-ordering/internal/caller.go new file mode 100644 index 000000000000..c2aeb2341122 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/caller.go @@ -0,0 +1,311 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "strings" + + "github.com/go-deterministic-ordering/fern/core" +) + +const ( + // contentType specifies the JSON Content-Type header value. + contentType = "application/json" + contentTypeHeader = "Content-Type" + contentTypeFormURLEncoded = "application/x-www-form-urlencoded" +) + +// Caller calls APIs and deserializes their response, if any. +type Caller struct { + client core.HTTPClient + retrier *Retrier +} + +// CallerParams represents the parameters used to constrcut a new *Caller. +type CallerParams struct { + Client core.HTTPClient + MaxAttempts uint +} + +// NewCaller returns a new *Caller backed by the given parameters. +func NewCaller(params *CallerParams) *Caller { + var httpClient core.HTTPClient = http.DefaultClient + if params.Client != nil { + httpClient = params.Client + } + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + return &Caller{ + client: httpClient, + retrier: NewRetrier(retryOptions...), + } +} + +// CallParams represents the parameters used to issue an API call. +type CallParams struct { + URL string + Method string + MaxAttempts uint + Headers http.Header + BodyProperties map[string]interface{} + QueryParameters url.Values + Client core.HTTPClient + Request interface{} + Response interface{} + ResponseIsOptional bool + ErrorDecoder ErrorDecoder +} + +// CallResponse is a parsed HTTP response from an API call. +type CallResponse struct { + StatusCode int + Header http.Header +} + +// Call issues an API call according to the given call parameters. +func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) { + url := buildURL(params.URL, params.QueryParameters) + req, err := newRequest( + ctx, + url, + params.Method, + params.Headers, + params.Request, + params.BodyProperties, + ) + if err != nil { + return nil, err + } + + // If the call has been cancelled, don't issue the request. + if err := ctx.Err(); err != nil { + return nil, 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 nil, err + } + + // Close the response body after we're done. + defer func() { _ = 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 nil, err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, 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 &CallResponse{ + StatusCode: resp.StatusCode, + Header: resp.Header, + }, nil + } + return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response) + } + return nil, err + } + } + + return &CallResponse{ + StatusCode: resp.StatusCode, + Header: resp.Header, + }, 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) { + // Determine the content type from headers, defaulting to JSON. + reqContentType := contentType + if endpointHeaders != nil { + if ct := endpointHeaders.Get(contentTypeHeader); ct != "" { + reqContentType = ct + } + } + requestBody, err := newRequestBody(request, bodyProperties, reqContentType) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, method, url, requestBody) + if err != nil { + return nil, err + } + req.Header.Set(contentTypeHeader, reqContentType) + 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{}, reqContentType string) (io.Reader, error) { + if isNil(request) { + if len(bodyProperties) == 0 { + return nil, nil + } + if reqContentType == contentTypeFormURLEncoded { + return newFormURLEncodedBody(bodyProperties), 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 + } + // Handle form URL encoded content type. + if reqContentType == contentTypeFormURLEncoded { + return newFormURLEncodedRequestBody(request, bodyProperties) + } + requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties) + if err != nil { + return nil, err + } + return bytes.NewReader(requestBytes), nil +} + +// newFormURLEncodedBody returns a new io.Reader that represents a form URL encoded body +// from the given body properties map. +func newFormURLEncodedBody(bodyProperties map[string]interface{}) io.Reader { + values := url.Values{} + for key, val := range bodyProperties { + values.Set(key, fmt.Sprintf("%v", val)) + } + return strings.NewReader(values.Encode()) +} + +// newFormURLEncodedRequestBody returns a new io.Reader that represents a form URL encoded body +// from the given request struct and body properties. +func newFormURLEncodedRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) { + values := url.Values{} + // Marshal the request to JSON first to respect any custom MarshalJSON methods, + // then unmarshal into a map to extract the field values. + jsonBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + var jsonMap map[string]interface{} + if err := json.Unmarshal(jsonBytes, &jsonMap); err != nil { + return nil, err + } + // Convert the JSON map to form URL encoded values. + for key, val := range jsonMap { + if val == nil { + continue + } + values.Set(key, fmt.Sprintf("%v", val)) + } + // Add any extra body properties. + for key, val := range bodyProperties { + values.Set(key, fmt.Sprintf("%v", val)) + } + return strings.NewReader(values.Encode()), 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.Header, response.Body) + } + // This endpoint doesn't have any custom error + // types, so we just read the body as-is, and + // put it into a normal error. + bytes, err := io.ReadAll(response.Body) + if err != nil && err != io.EOF { + return err + } + if err == io.EOF { + // The error didn't have a response body, + // so all we can do is return an error + // with the status code. + return core.NewAPIError(response.StatusCode, response.Header, nil) + } + return core.NewAPIError(response.StatusCode, response.Header, 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 { + if value == nil { + return true + } + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/caller_test.go b/seed/go-sdk/go-deterministic-ordering/internal/caller_test.go new file mode 100644 index 000000000000..1d3530b7f60b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/caller_test.go @@ -0,0 +1,705 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + + "github.com/go-deterministic-ordering/fern/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// InternalTestCase represents a single test case. +type InternalTestCase struct { + description string + + // Server-side assertions. + givePathSuffix string + giveMethod string + giveResponseIsOptional bool + giveHeader http.Header + giveErrorDecoder ErrorDecoder + giveRequest *InternalTestRequest + giveQueryParams url.Values + giveBodyProperties map[string]interface{} + + // Client-side assertions. + wantResponse *InternalTestResponse + wantError error +} + +// InternalTestRequest a simple request body. +type InternalTestRequest struct { + Id string `json:"id"` +} + +// InternalTestResponse a simple response body. +type InternalTestResponse struct { + Id string `json:"id"` + ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"` + QueryParameters url.Values `json:"queryParameters,omitempty"` +} + +// InternalTestNotFoundError represents a 404. +type InternalTestNotFoundError struct { + *core.APIError + + Message string `json:"message"` +} + +func TestCall(t *testing.T) { + tests := []*InternalTestCase{ + { + description: "GET success", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + wantResponse: &InternalTestResponse{ + Id: "123", + }, + }, + { + description: "GET success with query", + givePathSuffix: "?limit=1", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + wantResponse: &InternalTestResponse{ + Id: "123", + QueryParameters: url.Values{ + "limit": []string{"1"}, + }, + }, + }, + { + description: "GET not found", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &InternalTestRequest{ + Id: strconv.Itoa(http.StatusNotFound), + }, + giveErrorDecoder: newTestErrorDecoder(t), + wantError: &InternalTestNotFoundError{ + APIError: core.NewAPIError( + http.StatusNotFound, + http.Header{}, + errors.New(`{"message":"ID \"404\" not found"}`), + ), + }, + }, + { + description: "POST empty body", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: nil, + wantError: core.NewAPIError( + http.StatusBadRequest, + http.Header{}, + errors.New("invalid request"), + ), + }, + { + description: "POST optional response", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + giveResponseIsOptional: true, + }, + { + description: "POST API error", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &InternalTestRequest{ + Id: strconv.Itoa(http.StatusInternalServerError), + }, + wantError: core.NewAPIError( + http.StatusInternalServerError, + http.Header{}, + errors.New("failed to process request"), + ), + }, + { + description: "POST extra properties", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: new(InternalTestRequest), + giveBodyProperties: map[string]interface{}{ + "key": "value", + }, + wantResponse: &InternalTestResponse{ + 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: &InternalTestRequest{ + Id: "123", + }, + wantResponse: &InternalTestResponse{ + 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: &InternalTestRequest{ + Id: "123", + }, + giveQueryParams: url.Values{ + "extra": []string{"true"}, + }, + wantResponse: &InternalTestResponse{ + 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 *InternalTestResponse + _, 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 *InternalTestCase) *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(InternalTestRequest) + + 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 := &InternalTestNotFoundError{ + APIError: &core.APIError{ + StatusCode: http.StatusNotFound, + }, + Message: fmt.Sprintf("ID %q not found", request.Id), + } + bytes, err = json.Marshal(notFoundError) + require.NoError(t, err) + + w.WriteHeader(http.StatusNotFound) + _, err = w.Write(bytes) + require.NoError(t, err) + return + + case strconv.Itoa(http.StatusInternalServerError): + w.WriteHeader(http.StatusInternalServerError) + _, err = w.Write([]byte("failed to process request")) + require.NoError(t, err) + return + } + + if tc.giveResponseIsOptional { + w.WriteHeader(http.StatusOK) + return + } + + extraBodyProperties := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties)) + delete(extraBodyProperties, "id") + + response := &InternalTestResponse{ + 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) + }, + ), + ) +} + +func TestIsNil(t *testing.T) { + t.Run("nil interface", func(t *testing.T) { + assert.True(t, isNil(nil)) + }) + + t.Run("nil pointer", func(t *testing.T) { + var ptr *string + assert.True(t, isNil(ptr)) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + s := "test" + assert.False(t, isNil(&s)) + }) + + t.Run("nil slice", func(t *testing.T) { + var slice []string + assert.True(t, isNil(slice)) + }) + + t.Run("non-nil slice", func(t *testing.T) { + slice := []string{} + assert.False(t, isNil(slice)) + }) + + t.Run("nil map", func(t *testing.T) { + var m map[string]string + assert.True(t, isNil(m)) + }) + + t.Run("non-nil map", func(t *testing.T) { + m := make(map[string]string) + assert.False(t, isNil(m)) + }) + + t.Run("string value", func(t *testing.T) { + assert.False(t, isNil("test")) + }) + + t.Run("empty string value", func(t *testing.T) { + assert.False(t, isNil("")) + }) + + t.Run("int value", func(t *testing.T) { + assert.False(t, isNil(42)) + }) + + t.Run("zero int value", func(t *testing.T) { + assert.False(t, isNil(0)) + }) + + t.Run("bool value", func(t *testing.T) { + assert.False(t, isNil(true)) + }) + + t.Run("false bool value", func(t *testing.T) { + assert.False(t, isNil(false)) + }) + + t.Run("struct value", func(t *testing.T) { + type testStruct struct { + Field string + } + assert.False(t, isNil(testStruct{Field: "test"})) + }) + + t.Run("empty struct value", func(t *testing.T) { + type testStruct struct { + Field string + } + assert.False(t, isNil(testStruct{})) + }) +} + +// newTestErrorDecoder returns an error decoder suitable for tests. +func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error { + return func(statusCode int, header http.Header, body io.Reader) error { + raw, err := io.ReadAll(body) + require.NoError(t, err) + + var ( + apiError = core.NewAPIError(statusCode, header, errors.New(string(raw))) + decoder = json.NewDecoder(bytes.NewReader(raw)) + ) + if statusCode == http.StatusNotFound { + value := new(InternalTestNotFoundError) + value.APIError = apiError + require.NoError(t, decoder.Decode(value)) + + return value + } + return apiError + } +} + +// FormURLEncodedTestRequest is a test struct for form URL encoding tests. +type FormURLEncodedTestRequest struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + GrantType string `json:"grant_type,omitempty"` + Scope *string `json:"scope,omitempty"` + NilPointer *string `json:"nil_pointer,omitempty"` +} + +func TestNewFormURLEncodedBody(t *testing.T) { + t.Run("simple key-value pairs", func(t *testing.T) { + bodyProperties := map[string]interface{}{ + "client_id": "test_client_id", + "client_secret": "test_client_secret", + "grant_type": "client_credentials", + } + reader := newFormURLEncodedBody(bodyProperties) + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Parse the body and verify values + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "client_credentials", values.Get("grant_type")) + + // Verify it's not JSON + bodyStr := string(body) + assert.False(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should not be JSON, got: %s", bodyStr) + }) + + t.Run("special characters requiring URL encoding", func(t *testing.T) { + bodyProperties := map[string]interface{}{ + "value_with_space": "hello world", + "value_with_ampersand": "a&b", + "value_with_equals": "a=b", + "value_with_plus": "a+b", + } + reader := newFormURLEncodedBody(bodyProperties) + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Parse the body and verify values are correctly decoded + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "hello world", values.Get("value_with_space")) + assert.Equal(t, "a&b", values.Get("value_with_ampersand")) + assert.Equal(t, "a=b", values.Get("value_with_equals")) + assert.Equal(t, "a+b", values.Get("value_with_plus")) + }) + + t.Run("empty map", func(t *testing.T) { + bodyProperties := map[string]interface{}{} + reader := newFormURLEncodedBody(bodyProperties) + body, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Empty(t, string(body)) + }) +} + +func TestNewFormURLEncodedRequestBody(t *testing.T) { + t.Run("struct with json tags", func(t *testing.T) { + scope := "read write" + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + GrantType: "client_credentials", + Scope: &scope, + NilPointer: nil, + } + reader, err := newFormURLEncodedRequestBody(request, nil) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Parse the body and verify values + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "client_credentials", values.Get("grant_type")) + assert.Equal(t, "read write", values.Get("scope")) + // nil_pointer should not be present (nil pointer with omitempty) + assert.Empty(t, values.Get("nil_pointer")) + + // Verify it's not JSON + bodyStr := string(body) + assert.False(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should not be JSON, got: %s", bodyStr) + }) + + t.Run("struct with omitempty and zero values", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + GrantType: "", // empty string with omitempty should be omitted + Scope: nil, + NilPointer: nil, + } + reader, err := newFormURLEncodedRequestBody(request, nil) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + // grant_type should not be present (empty string with omitempty) + assert.Empty(t, values.Get("grant_type")) + assert.Empty(t, values.Get("scope")) + }) + + t.Run("struct with extra body properties", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + } + bodyProperties := map[string]interface{}{ + "extra_param": "extra_value", + } + reader, err := newFormURLEncodedRequestBody(request, bodyProperties) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "extra_value", values.Get("extra_param")) + }) + + t.Run("special characters in struct fields", func(t *testing.T) { + scope := "read&write=all+permissions" + request := &FormURLEncodedTestRequest{ + ClientID: "client with spaces", + ClientSecret: "secret&with=special+chars", + Scope: &scope, + } + reader, err := newFormURLEncodedRequestBody(request, nil) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "client with spaces", values.Get("client_id")) + assert.Equal(t, "secret&with=special+chars", values.Get("client_secret")) + assert.Equal(t, "read&write=all+permissions", values.Get("scope")) + }) +} + +func TestNewRequestBodyFormURLEncoded(t *testing.T) { + t.Run("selects form encoding when content-type is form-urlencoded", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + GrantType: "client_credentials", + } + reader, err := newRequestBody(request, nil, contentTypeFormURLEncoded) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Verify it's form-urlencoded, not JSON + bodyStr := string(body) + assert.False(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should not be JSON when Content-Type is form-urlencoded, got: %s", bodyStr) + + // Parse and verify values + values, err := url.ParseQuery(bodyStr) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "client_credentials", values.Get("grant_type")) + }) + + t.Run("selects JSON encoding when content-type is application/json", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + } + reader, err := newRequestBody(request, nil, contentType) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Verify it's JSON + bodyStr := string(body) + assert.True(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should be JSON when Content-Type is application/json, got: %s", bodyStr) + + // Parse and verify it's valid JSON + var parsed map[string]interface{} + err = json.Unmarshal(body, &parsed) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", parsed["client_id"]) + assert.Equal(t, "test_client_secret", parsed["client_secret"]) + }) + + t.Run("form encoding with body properties only (nil request)", func(t *testing.T) { + bodyProperties := map[string]interface{}{ + "client_id": "test_client_id", + "client_secret": "test_client_secret", + } + reader, err := newRequestBody(nil, bodyProperties, contentTypeFormURLEncoded) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/error_decoder.go b/seed/go-sdk/go-deterministic-ordering/internal/error_decoder.go new file mode 100644 index 000000000000..171123362617 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/error_decoder.go @@ -0,0 +1,64 @@ +package internal + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/go-deterministic-ordering/fern/core" +) + +// ErrorCodes maps HTTP status codes to error constructors. +type ErrorCodes map[int]func(*core.APIError) error + +// ErrorDecoder decodes *http.Response errors and returns a +// typed API error (e.g. *core.APIError). +type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error + +// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes. +// errorCodesOverrides is optional and will be merged with the default error codes, +// with overrides taking precedence. +func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder { + // Merge default error codes with overrides + mergedErrorCodes := make(ErrorCodes) + + // Start with default error codes + for statusCode, errorFunc := range errorCodes { + mergedErrorCodes[statusCode] = errorFunc + } + + // Apply overrides if provided + if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil { + for statusCode, errorFunc := range errorCodesOverrides[0] { + mergedErrorCodes[statusCode] = errorFunc + } + } + + return func(statusCode int, header http.Header, body io.Reader) error { + raw, err := io.ReadAll(body) + if err != nil { + return fmt.Errorf("failed to read error from response body: %w", err) + } + apiError := core.NewAPIError( + statusCode, + header, + errors.New(string(raw)), + ) + newErrorFunc, ok := mergedErrorCodes[statusCode] + if !ok { + // This status code isn't recognized, so we return + // the API error as-is. + return apiError + } + customError := newErrorFunc(apiError) + if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil { + // If we fail to decode the error, we return the + // API error as-is. + return apiError + } + return customError + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/error_decoder_test.go b/seed/go-sdk/go-deterministic-ordering/internal/error_decoder_test.go new file mode 100644 index 000000000000..2dbb5f5e65bd --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/error_decoder_test.go @@ -0,0 +1,59 @@ +package internal + +import ( + "bytes" + "errors" + "net/http" + "testing" + + "github.com/go-deterministic-ordering/fern/core" + "github.com/stretchr/testify/assert" +) + +func TestErrorDecoder(t *testing.T) { + decoder := NewErrorDecoder( + ErrorCodes{ + http.StatusNotFound: func(apiError *core.APIError) error { + return &InternalTestNotFoundError{APIError: apiError} + }, + }) + + tests := []struct { + description string + giveStatusCode int + giveHeader http.Header + giveBody string + wantError error + }{ + { + description: "unrecognized status code", + giveStatusCode: http.StatusInternalServerError, + giveHeader: http.Header{}, + giveBody: "Internal Server Error", + wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")), + }, + { + description: "not found with valid JSON", + giveStatusCode: http.StatusNotFound, + giveHeader: http.Header{}, + giveBody: `{"message": "Resource not found"}`, + wantError: &InternalTestNotFoundError{ + APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)), + Message: "Resource not found", + }, + }, + { + description: "not found with invalid JSON", + giveStatusCode: http.StatusNotFound, + giveHeader: http.Header{}, + giveBody: `Resource not found`, + wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody)))) + }) + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/explicit_fields.go b/seed/go-sdk/go-deterministic-ordering/internal/explicit_fields.go new file mode 100644 index 000000000000..4bdf34fc2b7c --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/explicit_fields.go @@ -0,0 +1,116 @@ +package internal + +import ( + "math/big" + "reflect" + "strings" +) + +// HandleExplicitFields processes a struct to remove `omitempty` from +// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields). +// Note that `marshaler` should be an embedded struct to avoid infinite recursion. +// Returns an interface{} that can be passed to json.Marshal. +func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} { + val := reflect.ValueOf(marshaler) + typ := reflect.TypeOf(marshaler) + + // Handle pointer types + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil + } + val = val.Elem() + typ = typ.Elem() + } + + // Only handle struct types + if val.Kind() != reflect.Struct { + return marshaler + } + + // Handle embedded struct pattern + var sourceVal reflect.Value + var sourceType reflect.Type + + // Check if this is an embedded struct pattern + if typ.NumField() == 1 && typ.Field(0).Anonymous { + // This is likely an embedded struct, get the embedded value + embeddedField := val.Field(0) + sourceVal = embeddedField + sourceType = embeddedField.Type() + } else { + // Regular struct + sourceVal = val + sourceType = typ + } + + // If no explicit fields set, use standard marshaling + if explicitFields == nil || explicitFields.Sign() == 0 { + return marshaler + } + + // Create a new struct type with modified tags + fields := make([]reflect.StructField, 0, sourceType.NumField()) + + for i := 0; i < sourceType.NumField(); i++ { + field := sourceType.Field(i) + + // Skip unexported fields and the explicitFields field itself + if !field.IsExported() || field.Name == "explicitFields" { + continue + } + + // Check if this field has been explicitly set + fieldBit := big.NewInt(1) + fieldBit.Lsh(fieldBit, uint(i)) + if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 { + // Remove omitempty from the json tag + tag := field.Tag.Get("json") + if tag != "" && tag != "-" { + // Parse the json tag, remove omitempty from options + parts := strings.Split(tag, ",") + if len(parts) > 1 { + var newParts []string + newParts = append(newParts, parts[0]) // Keep the field name + for _, part := range parts[1:] { + if strings.TrimSpace(part) != "omitempty" { + newParts = append(newParts, part) + } + } + tag = strings.Join(newParts, ",") + } + + // Reconstruct the struct tag + newTag := `json:"` + tag + `"` + if urlTag := field.Tag.Get("url"); urlTag != "" { + newTag += ` url:"` + urlTag + `"` + } + + field.Tag = reflect.StructTag(newTag) + } + } + + fields = append(fields, field) + } + + // Create new struct type with modified tags + newType := reflect.StructOf(fields) + newVal := reflect.New(newType).Elem() + + // Copy field values from original struct to new struct + fieldIndex := 0 + for i := 0; i < sourceType.NumField(); i++ { + originalField := sourceType.Field(i) + + // Skip unexported fields and the explicitFields field itself + if !originalField.IsExported() || originalField.Name == "explicitFields" { + continue + } + + originalValue := sourceVal.Field(i) + newVal.Field(fieldIndex).Set(originalValue) + fieldIndex++ + } + + return newVal.Interface() +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/explicit_fields_test.go b/seed/go-sdk/go-deterministic-ordering/internal/explicit_fields_test.go new file mode 100644 index 000000000000..f44beec447d6 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/explicit_fields_test.go @@ -0,0 +1,645 @@ +package internal + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testExplicitFieldsStruct struct { + Name *string `json:"name,omitempty"` + Code *string `json:"code,omitempty"` + Count *int `json:"count,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Tags []string `json:"tags,omitempty"` + unexported string `json:"-"` //nolint:unused + explicitFields *big.Int `json:"-"` +} + +var ( + testFieldName = big.NewInt(1 << 0) + testFieldCode = big.NewInt(1 << 1) + testFieldCount = big.NewInt(1 << 2) + testFieldEnabled = big.NewInt(1 << 3) + testFieldTags = big.NewInt(1 << 4) +) + +func (t *testExplicitFieldsStruct) require(field *big.Int) { + if t.explicitFields == nil { + t.explicitFields = big.NewInt(0) + } + t.explicitFields.Or(t.explicitFields, field) +} + +func (t *testExplicitFieldsStruct) SetName(name *string) { + t.Name = name + t.require(testFieldName) +} + +func (t *testExplicitFieldsStruct) SetCode(code *string) { + t.Code = code + t.require(testFieldCode) +} + +func (t *testExplicitFieldsStruct) SetCount(count *int) { + t.Count = count + t.require(testFieldCount) +} + +func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) { + t.Enabled = enabled + t.require(testFieldEnabled) +} + +func (t *testExplicitFieldsStruct) SetTags(tags []string) { + t.Tags = tags + t.require(testFieldTags) +} + +func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) { + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*t), + } + return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields)) +} + +type testStructWithoutExplicitFields struct { + Name *string `json:"name,omitempty"` + Code *string `json:"code,omitempty"` +} + +func TestHandleExplicitFields(t *testing.T) { + tests := []struct { + desc string + giveInput interface{} + wantBytes []byte + wantError string + }{ + { + desc: "nil input", + giveInput: nil, + wantBytes: []byte(`null`), + }, + { + desc: "non-struct input", + giveInput: "string", + wantBytes: []byte(`"string"`), + }, + { + desc: "slice input", + giveInput: []string{"a", "b"}, + wantBytes: []byte(`["a","b"]`), + }, + { + desc: "map input", + giveInput: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "struct without explicitFields field", + giveInput: &testStructWithoutExplicitFields{ + Name: stringPtr("test"), + Code: nil, + }, + wantBytes: []byte(`{"name":"test"}`), + }, + { + desc: "struct with no explicit fields set", + giveInput: &testExplicitFieldsStruct{ + Name: stringPtr("test"), + Code: nil, + }, + wantBytes: []byte(`{"name":"test"}`), + }, + { + desc: "struct with explicit nil field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Name: stringPtr("test"), + } + s.SetCode(nil) + return s + }(), + wantBytes: []byte(`{"name":"test","code":null}`), + }, + { + desc: "struct with explicit non-nil field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{} + s.SetName(stringPtr("explicit")) + s.SetCode(stringPtr("also-explicit")) + return s + }(), + wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`), + }, + { + desc: "struct with mixed explicit and implicit fields", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Name: stringPtr("implicit"), + Count: intPtr(42), + } + s.SetCode(nil) // explicit nil + return s + }(), + wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`), + }, + { + desc: "struct with multiple explicit nil fields", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Name: stringPtr("test"), + } + s.SetCode(nil) + s.SetCount(nil) + return s + }(), + wantBytes: []byte(`{"name":"test","code":null,"count":null}`), + }, + { + desc: "struct with slice field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Tags: []string{"tag1", "tag2"}, + } + s.SetTags(nil) // explicit nil slice + return s + }(), + wantBytes: []byte(`{"tags":null}`), + }, + { + desc: "struct with boolean field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{} + s.SetEnabled(boolPtr(false)) // explicit false + return s + }(), + wantBytes: []byte(`{"enabled":false}`), + }, + { + desc: "struct with all fields explicit", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{} + s.SetName(stringPtr("test")) + s.SetCode(nil) + s.SetCount(intPtr(0)) + s.SetEnabled(boolPtr(false)) + s.SetTags([]string{}) + return s + }(), + wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`), + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + var explicitFields *big.Int + if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok { + explicitFields = s.explicitFields + } + bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields)) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.JSONEq(t, string(tt.wantBytes), string(bytes)) + + // Verify it's valid JSON + var value interface{} + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) { + t.Run("custom marshaler with explicit fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + s.SetName(nil) + s.SetCode(stringPtr("test-code")) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes)) + }) + + t.Run("custom marshaler with no explicit fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("implicit"), + Code: stringPtr("also-implicit"), + } + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes)) + }) +} + +func TestHandleExplicitFieldsPointerHandling(t *testing.T) { + t.Run("nil pointer", func(t *testing.T) { + var s *testExplicitFieldsStruct + bytes, err := json.Marshal(HandleExplicitFields(s, nil)) + require.NoError(t, err) + assert.Equal(t, []byte(`null`), bytes) + }) + + t.Run("pointer to struct", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + s.SetName(nil) + + bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields)) + require.NoError(t, err) + assert.JSONEq(t, `{"name":null}`, string(bytes)) + }) +} + +func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) { + t.Run("embedded struct with explicit fields", func(t *testing.T) { + // Create a struct similar to what MarshalJSON creates + s := &testExplicitFieldsStruct{} + s.SetName(nil) + s.SetCode(stringPtr("test-code")) + + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*s), + } + + bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields)) + require.NoError(t, err) + // Should include both explicit fields (name as null, code as "test-code") + assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes)) + }) + + t.Run("embedded struct with no explicit fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("implicit"), + Code: stringPtr("also-implicit"), + } + + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*s), + } + + bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields)) + require.NoError(t, err) + // Should only include non-nil fields (omitempty behavior) + assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes)) + }) + + t.Run("embedded struct with mixed fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Count: intPtr(42), // implicit field + } + s.SetName(nil) // explicit nil + s.SetCode(stringPtr("explicit")) // explicit value + + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*s), + } + + bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields)) + require.NoError(t, err) + // Should include explicit null, explicit value, and implicit value + assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes)) + }) +} + +func TestHandleExplicitFieldsTagHandling(t *testing.T) { + type testStructWithComplexTags struct { + Field1 *string `json:"field1,omitempty" url:"field1,omitempty"` + Field2 *string `json:"field2,omitempty,string" url:"field2"` + Field3 *string `json:"-"` + Field4 *string `json:"field4"` + explicitFields *big.Int `json:"-"` + } + + s := &testStructWithComplexTags{ + Field1: stringPtr("test1"), + Field4: stringPtr("test4"), + explicitFields: big.NewInt(1), // Only first field is explicit + } + + bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields)) + require.NoError(t, err) + + // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included + assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes)) +} + +// Test types for nested struct explicit fields testing +type testNestedStruct struct { + NestedName *string `json:"nested_name,omitempty"` + NestedCode *string `json:"nested_code,omitempty"` + explicitFields *big.Int `json:"-"` +} + +type testParentStruct struct { + ParentName *string `json:"parent_name,omitempty"` + Nested *testNestedStruct `json:"nested,omitempty"` + explicitFields *big.Int `json:"-"` +} + +var ( + nestedFieldName = big.NewInt(1 << 0) + nestedFieldCode = big.NewInt(1 << 1) +) + +var ( + parentFieldName = big.NewInt(1 << 0) + parentFieldNested = big.NewInt(1 << 1) +) + +func (n *testNestedStruct) require(field *big.Int) { + if n.explicitFields == nil { + n.explicitFields = big.NewInt(0) + } + n.explicitFields.Or(n.explicitFields, field) +} + +func (n *testNestedStruct) SetNestedName(name *string) { + n.NestedName = name + n.require(nestedFieldName) +} + +func (n *testNestedStruct) SetNestedCode(code *string) { + n.NestedCode = code + n.require(nestedFieldCode) +} + +func (n *testNestedStruct) MarshalJSON() ([]byte, error) { + type embed testNestedStruct + var marshaler = struct { + embed + }{ + embed: embed(*n), + } + return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields)) +} + +func (p *testParentStruct) require(field *big.Int) { + if p.explicitFields == nil { + p.explicitFields = big.NewInt(0) + } + p.explicitFields.Or(p.explicitFields, field) +} + +func (p *testParentStruct) SetParentName(name *string) { + p.ParentName = name + p.require(parentFieldName) +} + +func (p *testParentStruct) SetNested(nested *testNestedStruct) { + p.Nested = nested + p.require(parentFieldNested) +} + +func (p *testParentStruct) MarshalJSON() ([]byte, error) { + type embed testParentStruct + var marshaler = struct { + embed + }{ + embed: embed(*p), + } + return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields)) +} + +func TestHandleExplicitFieldsNestedStruct(t *testing.T) { + tests := []struct { + desc string + setupFunc func() *testParentStruct + wantBytes []byte + }{ + { + desc: "nested struct with explicit nil in nested object", + setupFunc: func() *testParentStruct { + nested := &testNestedStruct{ + NestedName: stringPtr("implicit-nested"), + } + nested.SetNestedCode(nil) // explicit nil + + return &testParentStruct{ + ParentName: stringPtr("implicit-parent"), + Nested: nested, + } + }, + wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`), + }, + { + desc: "parent with explicit nil nested struct", + setupFunc: func() *testParentStruct { + parent := &testParentStruct{ + ParentName: stringPtr("implicit-parent"), + } + parent.SetNested(nil) // explicit nil nested struct + return parent + }, + wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`), + }, + { + desc: "all explicit fields in nested structure", + setupFunc: func() *testParentStruct { + nested := &testNestedStruct{} + nested.SetNestedName(stringPtr("explicit-nested")) + nested.SetNestedCode(nil) // explicit nil + + parent := &testParentStruct{} + parent.SetParentName(nil) // explicit nil + parent.SetNested(nested) // explicit nested struct + + return parent + }, + wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`), + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + parent := tt.setupFunc() + bytes, err := parent.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, string(tt.wantBytes), string(bytes)) + + // Verify it's valid JSON + var value interface{} + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +// Test for setter method documentation and behavior +func TestSetterMethodsDocumentation(t *testing.T) { + t.Run("setter prevents omitempty for nil values", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Use setter to explicitly set nil - this should prevent omitempty + s.SetName(nil) + s.SetCode(nil) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Both fields should be included as null, not omitted + assert.JSONEq(t, `{"name":null,"code":null}`, string(bytes)) + }) + + t.Run("setter prevents omitempty for empty slice", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Use setter to explicitly set empty slice + s.SetTags([]string{}) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Empty slice should be included as [], not omitted + assert.JSONEq(t, `{"tags":[]}`, string(bytes)) + }) + + t.Run("setter prevents omitempty for zero values", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Use setter to explicitly set zero values + s.SetCount(intPtr(0)) + s.SetEnabled(boolPtr(false)) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Zero values should be included, not omitted + assert.JSONEq(t, `{"count":0,"enabled":false}`, string(bytes)) + }) + + t.Run("direct assignment is omitted when nil", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: nil, // Direct assignment, not using setter + Code: nil, // Direct assignment, not using setter + } + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Fields not set via setter should be omitted when nil + assert.JSONEq(t, `{}`, string(bytes)) + }) + + t.Run("mix of setter and direct assignment", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("direct"), // Direct assignment + Count: intPtr(42), // Direct assignment + } + s.SetCode(nil) // Setter with nil + s.SetEnabled(boolPtr(false)) // Setter with zero value + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Direct assignments included if non-nil, setter fields always included + assert.JSONEq(t, `{"name":"direct","code":null,"count":42,"enabled":false}`, string(bytes)) + }) +} + +// Test for complex scenarios with multiple setters +func TestComplexSetterScenarios(t *testing.T) { + t.Run("multiple setter calls on same field", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Call setter multiple times - last one should win + s.SetName(stringPtr("first")) + s.SetName(stringPtr("second")) + s.SetName(nil) // Final value is nil + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Should serialize the last set value (nil) + assert.JSONEq(t, `{"name":null}`, string(bytes)) + }) + + t.Run("setter after direct assignment", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("direct"), + } + + // Override with setter + s.SetName(nil) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Setter should mark field as explicit, so nil is serialized + assert.JSONEq(t, `{"name":null}`, string(bytes)) + }) + + t.Run("all fields set via setters", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + s.SetName(nil) + s.SetCode(stringPtr("")) // Empty string + s.SetCount(intPtr(0)) // Zero + s.SetEnabled(boolPtr(false)) // False + s.SetTags(nil) // Nil slice + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // All fields should be present even with nil/zero values + assert.JSONEq(t, `{"name":null,"code":"","count":0,"enabled":false,"tags":null}`, string(bytes)) + }) +} + +// Test for backwards compatibility +func TestBackwardsCompatibility(t *testing.T) { + t.Run("struct without setters behaves normally", func(t *testing.T) { + s := &testStructWithoutExplicitFields{ + Name: stringPtr("test"), + Code: nil, // This should be omitted + } + + bytes, err := json.Marshal(s) + require.NoError(t, err) + + // Without setters, omitempty works normally + assert.JSONEq(t, `{"name":"test"}`, string(bytes)) + }) + + t.Run("struct with explicit fields works with standard json.Marshal", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("test"), + } + s.SetCode(nil) + + // Using the custom MarshalJSON + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + assert.JSONEq(t, `{"name":"test","code":null}`, string(bytes)) + }) +} + +// Helper functions +func stringPtr(s string) *string { + return &s +} + +func intPtr(i int) *int { + return &i +} + +func boolPtr(b bool) *bool { + return &b +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/extra_properties.go b/seed/go-sdk/go-deterministic-ordering/internal/extra_properties.go new file mode 100644 index 000000000000..540c3fd89eeb --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/extra_properties.go @@ -0,0 +1,141 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) { + bytes, err := json.Marshal(marshaler) + if err != nil { + return nil, err + } + if len(extraProperties) == 0 { + return bytes, nil + } + keys, err := getKeys(marshaler) + if err != nil { + return nil, err + } + for _, key := range keys { + if _, ok := extraProperties[key]; ok { + return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key) + } + } + extraBytes, err := json.Marshal(extraProperties) + if err != nil { + return nil, err + } + if isEmptyJSON(bytes) { + if isEmptyJSON(extraBytes) { + return bytes, nil + } + return extraBytes, nil + } + result := bytes[:len(bytes)-1] + result = append(result, ',') + result = append(result, extraBytes[1:len(extraBytes)-1]...) + result = append(result, '}') + return result, nil +} + +// ExtractExtraProperties extracts any extra properties from the given value. +func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) { + val := reflect.ValueOf(value) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, fmt.Errorf("value must be non-nil to extract extra properties") + } + val = val.Elem() + } + if err := json.Unmarshal(bytes, &value); err != nil { + return nil, err + } + var extraProperties map[string]interface{} + if err := json.Unmarshal(bytes, &extraProperties); err != nil { + return nil, err + } + for i := 0; i < val.Type().NumField(); i++ { + key := jsonKey(val.Type().Field(i)) + if key == "" || key == "-" { + continue + } + delete(extraProperties, key) + } + for _, key := range exclude { + delete(extraProperties, key) + } + if len(extraProperties) == 0 { + return nil, nil + } + return extraProperties, nil +} + +// getKeys returns the keys associated with the given value. The value must be a +// a struct or a map with string keys. +func getKeys(value interface{}) ([]string, error) { + val := reflect.ValueOf(value) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return nil, nil + } + switch val.Kind() { + case reflect.Struct: + return getKeysForStructType(val.Type()), nil + case reflect.Map: + var keys []string + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } + for _, key := range val.MapKeys() { + keys = append(keys, key.String()) + } + return keys, nil + default: + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } +} + +// getKeysForStructType returns all the keys associated with the given struct type, +// visiting embedded fields recursively. +func getKeysForStructType(structType reflect.Type) []string { + if structType.Kind() == reflect.Pointer { + structType = structType.Elem() + } + if structType.Kind() != reflect.Struct { + return nil + } + var keys []string + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous { + keys = append(keys, getKeysForStructType(field.Type)...) + continue + } + keys = append(keys, jsonKey(field)) + } + return keys +} + +// jsonKey returns the JSON key from the struct tag of the given field, +// excluding the omitempty flag (if any). +func jsonKey(field reflect.StructField) string { + return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") +} + +// isEmptyJSON returns true if the given data is empty, the empty JSON object, or +// an explicit null. +func isEmptyJSON(data []byte) bool { + return len(data) <= 2 || bytes.Equal(data, []byte("null")) +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/extra_properties_test.go b/seed/go-sdk/go-deterministic-ordering/internal/extra_properties_test.go new file mode 100644 index 000000000000..aa2510ee5121 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/extra_properties_test.go @@ -0,0 +1,228 @@ +package internal + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler interface{} + giveExtraProperties map[string]interface{} + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]interface{}{42: "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`, + }, + { + desc: "invalid map overwrite", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot add extra property "key" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"}, + wantError: `cannot add extra property "birthDate" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite embedded type", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"name": "bob"}, + wantError: `cannot add extra property "name" because it is already defined on the type`, + }, + { + desc: "nil", + giveMarshaler: nil, + giveExtraProperties: nil, + wantBytes: []byte(`null`), + }, + { + desc: "empty", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "metadata": map[string]interface{}{ + "ip": "127.0.0.1", + }, + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`), + }, + { + desc: "custom marshaler", + giveMarshaler: &testMarshaler{ + Name: "alice", + BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + giveExtraProperties: map[string]interface{}{ + "extra": "property", + }, + wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantBytes, bytes) + + value := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestExtractExtraProperties(t *testing.T) { + t.Run("none", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value) + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value *user + _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + assert.EqualError(t, err, "value must be non-nil to extract extra properties") + }) + + t.Run("non-zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value user + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("exclude", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age") + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/http.go b/seed/go-sdk/go-deterministic-ordering/internal/http.go new file mode 100644 index 000000000000..77863752bb58 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/http.go @@ -0,0 +1,71 @@ +package internal + +import ( + "fmt" + "net/http" + "net/url" + "reflect" +) + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// ResolveBaseURL resolves the base URL from the given arguments, +// preferring the first non-empty value. +func ResolveBaseURL(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} + +// EncodeURL encodes the given arguments into the URL, escaping +// values as needed. Pointer arguments are dereferenced before processing. +func EncodeURL(urlFormat string, args ...interface{}) string { + escapedArgs := make([]interface{}, 0, len(args)) + for _, arg := range args { + // Dereference the argument if it's a pointer + value := dereferenceArg(arg) + escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value))) + } + return fmt.Sprintf(urlFormat, escapedArgs...) +} + +// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value. +// If the argument is not a pointer or is nil, it returns the argument as-is. +func dereferenceArg(arg interface{}) interface{} { + if arg == nil { + return arg + } + + v := reflect.ValueOf(arg) + + // Keep dereferencing until we get to a non-pointer value or hit nil + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + return v.Interface() +} + +// MergeHeaders merges the given headers together, where the right +// takes precedence over the left. +func MergeHeaders(left, right http.Header) http.Header { + for key, values := range right { + if len(values) > 1 { + left[key] = values + continue + } + if value := right.Get(key); value != "" { + left.Set(key, value) + } + } + return left +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/pager.go b/seed/go-sdk/go-deterministic-ordering/internal/pager.go new file mode 100644 index 000000000000..7150f285040f --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/pager.go @@ -0,0 +1,121 @@ +package internal + +import ( + "context" + + "github.com/go-deterministic-ordering/fern/core" +) + +// PagerMode represents the different types of pagination modes. +type PagerMode uint + +// The available set of pagination modes. +const ( + PagerModeCursor PagerMode = iota + 1 + PagerModeOffset +) + +// Pager is the primary abstraction used to call paginated APIs. +type Pager[ + Cursor comparable, + Response any, + Results any, +] struct { + mode PagerMode + caller *Caller + prepareCall PageRequestFunc[Cursor] + readPageResponse PageResponseFunc[Cursor, Response, Results] +} + +// PageRequestFunc prepares the *CallParams from the given page request. +type PageRequestFunc[Cursor comparable] func(request *core.PageRequest[Cursor]) *CallParams + +// PageResponseFunc extracts the next page information from the response. +type PageResponseFunc[ + Cursor comparable, + Response any, + Results any, +] func(Response) *core.PageResponse[Cursor, Results, Response] + +// NewCursorPager constructs a new Pager that fetches pages from a paginated endpoint. +func NewCursorPager[Cursor comparable, Response any, Results any]( + caller *Caller, + prepareCall PageRequestFunc[Cursor], + readPageResponse PageResponseFunc[Cursor, Response, Results], +) *Pager[Cursor, Response, Results] { + return &Pager[Cursor, Response, Results]{ + mode: PagerModeCursor, + caller: caller, + prepareCall: prepareCall, + readPageResponse: readPageResponse, + } +} + +// NewOffsetPager constructs a new Pager that fetches pages from an offset paginated endpoint. +func NewOffsetPager[Cursor comparable, Response any, Results any]( + caller *Caller, + prepareCall PageRequestFunc[Cursor], + readPageResponse PageResponseFunc[Cursor, Response, Results], +) *Pager[Cursor, Response, Results] { + return &Pager[Cursor, Response, Results]{ + mode: PagerModeOffset, + caller: caller, + prepareCall: prepareCall, + readPageResponse: readPageResponse, + } +} + +// GetPage retrieves the page associated with the given cursor. +func (p *Pager[ + Cursor, + Response, + Results, +]) GetPage(ctx context.Context, cursor Cursor) (*core.Page[Cursor, Results, Response], error) { + var response Response + pageRequest := &core.PageRequest[Cursor]{ + Cursor: cursor, + Response: &response, + } + + callParams := p.prepareCall(pageRequest) + httpResponse, err := p.caller.Call(ctx, callParams) + if err != nil { + return nil, err + } + + pageResponse := p.readPageResponse(response) + + if p.mode == PagerModeOffset { + return &core.Page[Cursor, Results, Response]{ + Results: pageResponse.Results, + Response: pageResponse.Response, + RawResponse: *pageResponse, + StatusCode: httpResponse.StatusCode, + Header: httpResponse.Header, + NextPageFunc: func(ctx context.Context) (*core.Page[Cursor, Results, Response], error) { + page, err := p.GetPage(ctx, pageResponse.Next) + if err != nil { + return nil, err + } + if len(page.Results) == 0 { + return nil, core.ErrNoPages + } + return page, nil + }, + }, nil + } + + return &core.Page[Cursor, Results, Response]{ + Results: pageResponse.Results, + Response: pageResponse.Response, + RawResponse: *pageResponse, + StatusCode: httpResponse.StatusCode, + Header: httpResponse.Header, + NextPageFunc: func(ctx context.Context) (*core.Page[Cursor, Results, Response], error) { + if pageResponse.Done { + return nil, core.ErrNoPages + } + return p.GetPage(ctx, pageResponse.Next) + }, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/pager_test.go b/seed/go-sdk/go-deterministic-ordering/internal/pager_test.go new file mode 100644 index 000000000000..1789a54a1e46 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/pager_test.go @@ -0,0 +1,171 @@ +package internal + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-deterministic-ordering/fern/core" +) + +type TestPageResponse struct { + Items []TestPageItem `json:"items"` + Next string `json:"next"` +} + +type TestPageItem struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func TestPager(t *testing.T) { + tests := []struct { + description string + givePages []TestPageResponse + giveCursor string + wantItems []TestPageItem + wantError error + }{ + { + description: "handles multiple pages successfully", + givePages: []TestPageResponse{ + { + Items: []TestPageItem{{ID: "1", Name: "First"}}, + Next: "abc", + }, + { + Items: []TestPageItem{{ID: "2", Name: "Second"}}, + Next: "def", + }, + { + Items: []TestPageItem{{ID: "3", Name: "Third"}}, + Next: "", + }, + }, + wantItems: []TestPageItem{ + {ID: "1", Name: "First"}, + {ID: "2", Name: "Second"}, + {ID: "3", Name: "Third"}, + }, + }, + { + description: "handles empty response", + givePages: []TestPageResponse{ + { + Items: []TestPageItem{}, + Next: "", + }, + }, + wantItems: nil, + }, + { + description: "handles single page", + givePages: []TestPageResponse{ + { + Items: []TestPageItem{{ID: "1", Name: "Only"}}, + Next: "", + }, + }, + wantItems: []TestPageItem{ + {ID: "1", Name: "Only"}, + }, + }, + { + description: "handles initial cursor", + giveCursor: "abc", + givePages: []TestPageResponse{ + { + Items: []TestPageItem{{ID: "1", Name: "First"}}, + Next: "", + }, + }, + wantItems: []TestPageItem{ + {ID: "1", Name: "First"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var pageIndex int + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if pageIndex >= len(tt.givePages) { + t.Fatal("requested more pages than available") + } + if pageIndex > 0 { + assert.Equal(t, tt.givePages[pageIndex-1].Next, r.URL.Query().Get("cursor")) + } + require.NoError(t, json.NewEncoder(w).Encode(tt.givePages[pageIndex])) + pageIndex++ + })) + defer server.Close() + + caller := NewCaller( + &CallerParams{ + Client: server.Client(), + }, + ) + pager := NewCursorPager( + caller, + func(request *core.PageRequest[*string]) *CallParams { + url := server.URL + if request.Cursor != nil { + url += "?cursor=" + *request.Cursor + } + return &CallParams{ + URL: url, + Method: http.MethodGet, + Response: request.Response, + } + }, + func(response *TestPageResponse) *core.PageResponse[*string, *TestPageItem, *TestPageResponse] { + var items []*TestPageItem + for _, item := range response.Items { + itemCopy := item + items = append(items, &itemCopy) + } + var next *string + if response.Next != "" { + next = &response.Next + } + return &core.PageResponse[*string, *TestPageItem, *TestPageResponse]{ + Results: items, + Response: response, + Next: next, + Done: response.Next == "", + } + }, + ) + + page, err := pager.GetPage(context.Background(), &tt.giveCursor) + if tt.wantError != nil { + assert.Equal(t, tt.wantError, err) + return + } + require.NoError(t, err) + + require.NotNil(t, page.Response) + if len(tt.givePages) > 0 { + assert.Equal(t, tt.givePages[0].Items, page.Response.Items) + assert.Equal(t, tt.givePages[0].Next, page.Response.Next) + } + + var items []TestPageItem + iter := page.Iterator() + for iter.Next(context.Background()) { + item := iter.Current() + items = append(items, TestPageItem{ + ID: item.ID, + Name: item.Name, + }) + } + require.NoError(t, iter.Err()) + assert.Equal(t, tt.wantItems, items) + }) + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/query.go b/seed/go-sdk/go-deterministic-ordering/internal/query.go new file mode 100644 index 000000000000..1cbaf7fe1c02 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/query.go @@ -0,0 +1,353 @@ +package internal + +import ( + "encoding/base64" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/google/uuid" +) + +var ( + bytesType = reflect.TypeOf([]byte{}) + queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem() + timeType = reflect.TypeOf(time.Time{}) + uuidType = reflect.TypeOf(uuid.UUID{}) +) + +// QueryEncoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type QueryEncoder interface { + EncodeQueryValues(key string, v *url.Values) error +} + +// prepareValue handles common validation and unwrapping logic for both functions +func prepareValue(v interface{}) (reflect.Value, url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return reflect.Value{}, values, nil + } + val = val.Elem() + } + + if v == nil { + return reflect.Value{}, values, nil + } + + if val.Kind() != reflect.Struct { + return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + if err != nil { + return reflect.Value{}, nil, err + } + + return val, values, nil +} + +// 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, err := prepareValue(v) + return values, err +} + +// QueryValuesWithDefaults encodes url.Values from request objects +// and default values, merging the defaults into the request. +// It's expected that the values of defaults are wire names. +func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) { + val, values, err := prepareValue(v) + if err != nil { + return values, err + } + if !val.IsValid() { + return values, nil + } + + // apply defaults to zero-value fields directly on the original struct + valType := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := valType.Field(i) + fieldName := fieldType.Name + + if fieldType.PkgPath != "" && !fieldType.Anonymous { + // Skip unexported fields. + continue + } + + // check if field is zero value and we have a default for it + if field.CanSet() && field.IsZero() { + tag := fieldType.Tag.Get("url") + if tag == "" || tag == "-" { + continue + } + wireName, _ := parseTag(tag) + if wireName == "" { + wireName = fieldName + } + if defaultVal, exists := defaults[wireName]; exists { + values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{})) + } + } + } + + 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.Map { + if err := reflectMap(values, sv, name); err != nil { + return err + } + 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 +} + +// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value +func reflectMap(values url.Values, val reflect.Value, scope string) error { + if val.IsNil() { + return nil + } + + iter := val.MapRange() + for iter.Next() { + k := iter.Key() + v := iter.Value() + + key := fmt.Sprint(k.Interface()) + paramName := scope + "[" + key + "]" + + for v.Kind() == reflect.Ptr { + if v.IsNil() { + break + } + v = v.Elem() + } + + for v.Kind() == reflect.Interface { + v = v.Elem() + } + + if v.Kind() == reflect.Map { + if err := reflectMap(values, v, paramName); err != nil { + return err + } + continue + } + + if v.Kind() == reflect.Struct { + if err := reflectValue(values, v, paramName); err != nil { + return err + } + continue + } + + if v.Kind() == reflect.Slice || v.Kind() == reflect.Array { + if v.Len() == 0 { + continue + } + for i := 0; i < v.Len(); i++ { + value := v.Index(i) + if isStructPointer(value) && !value.IsNil() { + if err := reflectValue(values, value.Elem(), paramName); err != nil { + return err + } + } else { + values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{})) + } + } + continue + } + + values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{})) + } + + 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/go-deterministic-ordering/internal/query_test.go b/seed/go-sdk/go-deterministic-ordering/internal/query_test.go new file mode 100644 index 000000000000..2c28cb8acf68 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/query_test.go @@ -0,0 +1,395 @@ +package internal + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQueryValues(t *testing.T) { + t.Run("empty optional", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Empty(t, values) + }) + + t.Run("empty required", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Equal(t, "required=", values.Encode()) + }) + + t.Run("allow multiple", func(t *testing.T) { + type example struct { + Values []string `json:"values" url:"values"` + } + + values, err := QueryValues( + &example{ + Values: []string{"foo", "bar", "baz"}, + }, + ) + require.NoError(t, err) + assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode()) + }) + + t.Run("nested object", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + nestedValue := "nestedValue" + values, err := QueryValues( + &example{ + Required: "requiredValue", + Nested: &nested{ + Value: &nestedValue, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode()) + }) + + t.Run("url unspecified", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("url ignored", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound" url:"-"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("datetime", func(t *testing.T) { + type example struct { + DateTime time.Time `json:"dateTime" url:"dateTime"` + } + + values, err := QueryValues( + &example{ + DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode()) + }) + + t.Run("date", func(t *testing.T) { + type example struct { + Date time.Time `json:"date" url:"date" format:"date"` + } + + values, err := QueryValues( + &example{ + Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "date=1994-03-16", values.Encode()) + }) + + t.Run("optional time", func(t *testing.T) { + type example struct { + Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("omitempty with non-pointer zero value", func(t *testing.T) { + type enum string + + type example struct { + Enum enum `json:"enum,omitempty" url:"enum,omitempty"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("object array", func(t *testing.T) { + type object struct { + Key string `json:"key" url:"key"` + Value string `json:"value" url:"value"` + } + type example struct { + Objects []*object `json:"objects,omitempty" url:"objects,omitempty"` + } + + values, err := QueryValues( + &example{ + Objects: []*object{ + { + Key: "hello", + Value: "world", + }, + { + Key: "foo", + Value: "bar", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode()) + }) + + t.Run("map", func(t *testing.T) { + type request struct { + Metadata map[string]interface{} `json:"metadata" url:"metadata"` + } + values, err := QueryValues( + &request{ + Metadata: map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode()) + }) + + t.Run("nested map", func(t *testing.T) { + type request struct { + Metadata map[string]interface{} `json:"metadata" url:"metadata"` + } + values, err := QueryValues( + &request{ + Metadata: map[string]interface{}{ + "inner": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode()) + }) + + t.Run("nested map array", func(t *testing.T) { + type request struct { + Metadata map[string]interface{} `json:"metadata" url:"metadata"` + } + values, err := QueryValues( + &request{ + Metadata: map[string]interface{}{ + "inner": []string{ + "one", + "two", + "three", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode()) + }) +} + +func TestQueryValuesWithDefaults(t *testing.T) { + t.Run("apply defaults to zero values", func(t *testing.T) { + type example struct { + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + Enabled bool `json:"enabled" url:"enabled"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "enabled": true, + } + + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode()) + }) + + t.Run("preserve non-zero values over defaults", func(t *testing.T) { + type example struct { + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + Enabled bool `json:"enabled" url:"enabled"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "enabled": true, + } + + values, err := QueryValuesWithDefaults(&example{ + Name: "actual-name", + Age: 30, + // Enabled remains false (zero value), should get default + }, defaults) + require.NoError(t, err) + assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode()) + }) + + t.Run("ignore defaults for fields not in struct", func(t *testing.T) { + type example struct { + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "nonexistent": "should-be-ignored", + } + + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "age=25&name=default-name", values.Encode()) + }) + + t.Run("type conversion for compatible defaults", func(t *testing.T) { + type example struct { + Count int64 `json:"count" url:"count"` + Rate float64 `json:"rate" url:"rate"` + Message string `json:"message" url:"message"` + } + + defaults := map[string]interface{}{ + "count": int(100), // int -> int64 conversion + "rate": float32(2.5), // float32 -> float64 conversion + "message": "hello", // string -> string (no conversion needed) + } + + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode()) + }) + + t.Run("mixed with pointer fields and omitempty", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + Optional *string `json:"optional,omitempty" url:"optional,omitempty"` + Count int `json:"count,omitempty" url:"count,omitempty"` + } + + defaultOptional := "default-optional" + defaults := map[string]interface{}{ + "required": "default-required", + "optional": &defaultOptional, // pointer type + "count": 42, + } + + values, err := QueryValuesWithDefaults(&example{ + Required: "custom-required", // should override default + // Optional is nil, should get default + // Count is 0, should get default + }, defaults) + require.NoError(t, err) + assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode()) + }) + + t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) { + type example struct { + Name *string `json:"name" url:"name"` + Age *int `json:"age" url:"age"` + Enabled *bool `json:"enabled" url:"enabled"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "enabled": true, + } + + // first, test that a properly empty request is overridden: + { + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode()) + } + + // second, test that a request that contains zeros is not overridden: + var ( + name = "" + age = 0 + enabled = false + ) + values, err := QueryValuesWithDefaults(&example{ + Name: &name, // explicit empty string should override default + Age: &age, // explicit zero should override default + Enabled: &enabled, // explicit false should override default + }, defaults) + require.NoError(t, err) + assert.Equal(t, "age=0&enabled=false&name=", values.Encode()) + }) + + t.Run("nil input returns empty values", func(t *testing.T) { + defaults := map[string]any{ + "name": "default-name", + "age": 25, + } + + // Test with nil + values, err := QueryValuesWithDefaults(nil, defaults) + require.NoError(t, err) + assert.Empty(t, values) + + // Test with nil pointer + type example struct { + Name string `json:"name" url:"name"` + } + var nilPtr *example + values, err = QueryValuesWithDefaults(nilPtr, defaults) + require.NoError(t, err) + assert.Empty(t, values) + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/retrier.go b/seed/go-sdk/go-deterministic-ordering/internal/retrier.go new file mode 100644 index 000000000000..02fd1fb7d3f1 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/retrier.go @@ -0,0 +1,239 @@ +package internal + +import ( + "crypto/rand" + "math/big" + "net/http" + "strconv" + "time" +) + +const ( + defaultRetryAttempts = 2 + minRetryDelay = 1000 * time.Millisecond + maxRetryDelay = 60000 * time.Millisecond +) + +// RetryOption adapts the behavior the *Retrier. +type RetryOption func(*retryOptions) + +// RetryFunc is a retryable 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 retryable 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 + } + + // Reset the request body for retries since the body may have already been read. + if retryAttempt > 0 && request.GetBody != nil { + requestBody, err := request.GetBody() + if err != nil { + return nil, err + } + request.Body = requestBody + } + + response, err := fn(request) + if err != nil { + return nil, err + } + + if r.shouldRetry(response) { + defer func() { _ = response.Body.Close() }() + + delay, err := r.retryDelay(response, retryAttempt) + if err != nil { + return nil, err + } + + time.Sleep(delay) + + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt+1, + decodeError(response, errorDecoder), + ) + } + + return response, nil +} + +// shouldRetry returns true if the request should be retried based on the given +// response status code. +func (r *Retrier) shouldRetry(response *http.Response) bool { + return response.StatusCode == http.StatusTooManyRequests || + response.StatusCode == http.StatusRequestTimeout || + response.StatusCode >= http.StatusInternalServerError +} + +// retryDelay calculates the delay time based on response headers, +// falling back to exponential backoff if no headers are present. +func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) { + // Check for Retry-After header first (RFC 7231), applying no jitter + if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" { + // Parse as number of seconds... + if seconds, err := strconv.Atoi(retryAfter); err == nil { + delay := time.Duration(seconds) * time.Second + if delay > 0 { + if delay > maxRetryDelay { + delay = maxRetryDelay + } + return delay, nil + } + } + + // ...or as an HTTP date; both are valid + if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil { + delay := time.Until(retryTime) + if delay > 0 { + if delay > maxRetryDelay { + delay = maxRetryDelay + } + return delay, nil + } + } + } + + // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter + if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" { + if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil { + // Assume Unix timestamp in seconds + resetTime := time.Unix(resetTimestamp, 0) + delay := time.Until(resetTime) + if delay > 0 { + if delay > maxRetryDelay { + delay = maxRetryDelay + } + return r.addPositiveJitter(delay) + } + } + } + + // Fall back to exponential backoff + return r.exponentialBackoff(retryAttempt) +} + +// exponentialBackoff calculates the delay time based on the retry attempt +// and applies symmetric jitter (±10% around the delay). +func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) { + if retryAttempt > 63 { // 2^63+ would overflow uint64 + retryAttempt = 63 + } + + delay := minRetryDelay << retryAttempt + if delay > maxRetryDelay { + delay = maxRetryDelay + } + + return r.addSymmetricJitter(delay) +} + +// addJitterWithRange applies jitter to the given delay. +// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%). +func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) { + jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100)) + jitter, err := rand.Int(rand.Reader, jitterRange) + if err != nil { + return 0, err + } + + jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100 + if jitteredDelay < minRetryDelay { + jitteredDelay = minRetryDelay + } + if jitteredDelay > maxRetryDelay { + jitteredDelay = maxRetryDelay + } + return jitteredDelay, nil +} + +// addPositiveJitter applies positive jitter to the given delay (100%-120% range). +func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) { + return r.addJitterWithRange(delay, 100, 120) +} + +// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range). +func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) { + return r.addJitterWithRange(delay, 90, 110) +} + +type retryOptions struct { + attempts uint +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/retrier_test.go b/seed/go-sdk/go-deterministic-ordering/internal/retrier_test.go new file mode 100644 index 000000000000..6969de2cb08a --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/retrier_test.go @@ -0,0 +1,352 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-deterministic-ordering/fern/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type RetryTestCase struct { + description string + + giveAttempts uint + giveStatusCodes []int + giveResponse *InternalTestResponse + + wantResponse *InternalTestResponse + wantError *core.APIError +} + +func TestRetrier(t *testing.T) { + tests := []*RetryTestCase{ + { + description: "retry request succeeds after multiple failures", + giveAttempts: 3, + giveStatusCodes: []int{ + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusOK, + }, + giveResponse: &InternalTestResponse{ + Id: "1", + }, + wantResponse: &InternalTestResponse{ + Id: "1", + }, + }, + { + description: "retry request fails if MaxAttempts is exceeded", + giveAttempts: 3, + giveStatusCodes: []int{ + http.StatusRequestTimeout, + http.StatusRequestTimeout, + http.StatusRequestTimeout, + http.StatusOK, + }, + wantError: &core.APIError{ + StatusCode: http.StatusRequestTimeout, + }, + }, + { + description: "retry durations increase exponentially and stay within the min and max delay values", + giveAttempts: 4, + giveStatusCodes: []int{ + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusOK, + }, + }, + { + description: "retry does not occur on status code 404", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusNotFound, http.StatusOK}, + wantError: &core.APIError{ + StatusCode: http.StatusNotFound, + }, + }, + { + description: "retries occur on status code 429", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK}, + }, + { + description: "retries occur on status code 408", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK}, + }, + { + description: "retries occur on status code 500", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK}, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + var ( + test = tc + server = newTestRetryServer(t, test) + client = server.Client() + ) + + t.Parallel() + + caller := NewCaller( + &CallerParams{ + Client: client, + }, + ) + + var response *InternalTestResponse + _, err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: http.MethodGet, + Request: &InternalTestRequest{}, + Response: &response, + MaxAttempts: test.giveAttempts, + ResponseIsOptional: true, + }, + ) + + if test.wantError != nil { + require.IsType(t, err, &core.APIError{}) + expectedErrorCode := test.wantError.StatusCode + actualErrorCode := err.(*core.APIError).StatusCode + assert.Equal(t, expectedErrorCode, actualErrorCode) + return + } + + require.NoError(t, err) + assert.Equal(t, test.wantResponse, response) + }) + } +} + +// newTestRetryServer returns a new *httptest.Server configured with the +// given test parameters, suitable for testing retries. +func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server { + var index int + timestamps := make([]time.Time, 0, len(tc.giveStatusCodes)) + + return httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + timestamps = append(timestamps, time.Now()) + if index > 0 && index < len(expectedRetryDurations) { + // Ensure that the duration between retries increases exponentially, + // and that it is within the minimum and maximum retry delay values. + actualDuration := timestamps[index].Sub(timestamps[index-1]) + expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100 + expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100 + assert.True( + t, + actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax, + "expected duration to be in range [%v, %v], got %v", + expectedDurationMin, + expectedDurationMax, + actualDuration, + ) + assert.LessOrEqual( + t, + actualDuration, + maxRetryDelay, + "expected duration to be less than the maxRetryDelay (%v), got %v", + maxRetryDelay, + actualDuration, + ) + assert.GreaterOrEqual( + t, + actualDuration, + minRetryDelay, + "expected duration to be greater than the minRetryDelay (%v), got %v", + minRetryDelay, + actualDuration, + ) + } + + request := new(InternalTestRequest) + bytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(bytes, request)) + require.LessOrEqual(t, index, len(tc.giveStatusCodes)) + + statusCode := tc.giveStatusCodes[index] + + w.WriteHeader(statusCode) + + if tc.giveResponse != nil && statusCode == http.StatusOK { + bytes, err = json.Marshal(tc.giveResponse) + require.NoError(t, err) + _, err = w.Write(bytes) + require.NoError(t, err) + } + + index++ + }, + ), + ) +} + +// expectedRetryDurations holds an array of calculated retry durations, +// where the index of the array should correspond to the retry attempt. +// +// Values are calculated based off of `minRetryDelay * 2^i`. +var expectedRetryDurations = []time.Duration{ + 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms + 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms + 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms + 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms +} + +func TestRetryWithRequestBody(t *testing.T) { + // This test verifies that POST requests with a body are properly retried. + // The request body should be re-sent on each retry attempt. + expectedBody := `{"id":"test-id"}` + var requestBodies []string + var requestCount int + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + bodyBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + requestBodies = append(requestBodies, string(bodyBytes)) + + if requestCount == 1 { + // First request - return retryable error + w.WriteHeader(http.StatusServiceUnavailable) + return + } + // Second request - return success + w.WriteHeader(http.StatusOK) + response := &InternalTestResponse{Id: "success"} + bytes, _ := json.Marshal(response) + _, _ = w.Write(bytes) + })) + defer server.Close() + + caller := NewCaller(&CallerParams{ + Client: server.Client(), + }) + + var response *InternalTestResponse + _, err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: http.MethodPost, + Request: &InternalTestRequest{Id: "test-id"}, + Response: &response, + MaxAttempts: 2, + ResponseIsOptional: true, + }, + ) + + require.NoError(t, err) + require.Equal(t, 2, requestCount, "Expected exactly 2 requests") + require.Len(t, requestBodies, 2, "Expected 2 request bodies to be captured") + + // Both requests should have the same non-empty body + assert.Equal(t, expectedBody, requestBodies[0], "First request body should match expected") + assert.Equal(t, expectedBody, requestBodies[1], "Second request body should match expected (retry should re-send body)") +} + +func TestRetryDelayTiming(t *testing.T) { + tests := []struct { + name string + headerName string + headerValueFunc func() string + expectedMinMs int64 + expectedMaxMs int64 + }{ + { + name: "retry-after with seconds value", + headerName: "retry-after", + headerValueFunc: func() string { + return "1" + }, + expectedMinMs: 500, + expectedMaxMs: 1500, + }, + { + name: "retry-after with HTTP date", + headerName: "retry-after", + headerValueFunc: func() string { + return time.Now().Add(3 * time.Second).Format(time.RFC1123) + }, + expectedMinMs: 1500, + expectedMaxMs: 4500, + }, + { + name: "x-ratelimit-reset with future timestamp", + headerName: "x-ratelimit-reset", + headerValueFunc: func() string { + return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix()) + }, + expectedMinMs: 1500, + expectedMaxMs: 4500, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var timestamps []time.Time + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + timestamps = append(timestamps, time.Now()) + if len(timestamps) == 1 { + // First request - return retryable error with header + w.Header().Set(tt.headerName, tt.headerValueFunc()) + w.WriteHeader(http.StatusTooManyRequests) + } else { + // Second request - return success + w.WriteHeader(http.StatusOK) + response := &InternalTestResponse{Id: "success"} + bytes, _ := json.Marshal(response) + _, _ = w.Write(bytes) + } + })) + defer server.Close() + + caller := NewCaller(&CallerParams{ + Client: server.Client(), + }) + + var response *InternalTestResponse + _, err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: http.MethodGet, + Request: &InternalTestRequest{}, + Response: &response, + MaxAttempts: 2, + ResponseIsOptional: true, + }, + ) + + require.NoError(t, err) + require.Len(t, timestamps, 2, "Expected exactly 2 requests") + + actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds() + + assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs, + "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs) + assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs, + "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs) + }) + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/stringer.go b/seed/go-sdk/go-deterministic-ordering/internal/stringer.go new file mode 100644 index 000000000000..312801851e0e --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/stringer.go @@ -0,0 +1,13 @@ +package internal + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value interface{}) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/internal/time.go b/seed/go-sdk/go-deterministic-ordering/internal/time.go new file mode 100644 index 000000000000..57f901a35ed8 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/internal/time.go @@ -0,0 +1,165 @@ +package internal + +import ( + "encoding/json" + "fmt" + "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 { + // If the value is not a string, check if it is a number (unix epoch seconds). + var epoch int64 + if numErr := json.Unmarshal(data, &epoch); numErr == nil { + t := time.Unix(epoch, 0).UTC() + *d = DateTime{t: &t} + return nil + } + return err + } + + // Try RFC3339Nano first (superset of RFC3339, supports fractional seconds). + parsedTime, err := time.Parse(time.RFC3339Nano, raw) + if err == nil { + *d = DateTime{t: &parsedTime} + return nil + } + rfc3339NanoErr := err + + // Fall back to ISO 8601 without timezone (assume UTC). + parsedTime, err = time.Parse("2006-01-02T15:04:05", raw) + if err == nil { + parsedTime = parsedTime.UTC() + *d = DateTime{t: &parsedTime} + return nil + } + iso8601Err := err + + // Fall back to date-only format. + parsedTime, err = time.Parse("2006-01-02", raw) + if err == nil { + parsedTime = parsedTime.UTC() + *d = DateTime{t: &parsedTime} + return nil + } + dateOnlyErr := err + + return fmt.Errorf("unable to parse datetime string %q: tried RFC3339Nano (%v), ISO8601 (%v), date-only (%v)", raw, rfc3339NanoErr, iso8601Err, dateOnlyErr) +} diff --git a/seed/go-sdk/go-deterministic-ordering/noauth/client.go b/seed/go-sdk/go-deterministic-ordering/noauth/client.go new file mode 100644 index 000000000000..ad5cea004cf4 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/noauth/client.go @@ -0,0 +1,49 @@ +// Code generated by Fern. DO NOT EDIT. + +package noauth + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// POST request with no auth +func (c *Client) PostWithNoAuth( + ctx context.Context, + request any, + opts ...option.RequestOption, +) (bool, error) { + response, err := c.WithRawResponse.PostWithNoAuth( + ctx, + request, + opts..., + ) + if err != nil { + return false, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/noauth/no_auth_test/no_auth_test.go b/seed/go-sdk/go-deterministic-ordering/noauth/no_auth_test/no_auth_test.go new file mode 100644 index 000000000000..087b04c06c69 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/noauth/no_auth_test/no_auth_test.go @@ -0,0 +1,87 @@ +// Code generated by Fern. DO NOT EDIT. + +package no_auth_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestNoAuthPostWithNoAuthWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := map[string]any{ + "key": "value", + } + _, invocationErr := client.NoAuth.PostWithNoAuth( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestNoAuthPostWithNoAuthWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestNoAuthPostWithNoAuthWithWireMock", "POST", "/no-auth", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/noauth/raw_client.go b/seed/go-sdk/go-deterministic-ordering/noauth/raw_client.go new file mode 100644 index 000000000000..8541d231f346 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/noauth/raw_client.go @@ -0,0 +1,73 @@ +// Code generated by Fern. DO NOT EDIT. + +package noauth + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) PostWithNoAuth( + ctx context.Context, + request any, + opts ...option.RequestOption, +) (*core.Response[bool], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/no-auth" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response bool + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + ErrorDecoder: internal.NewErrorDecoder(fern.ErrorCodes), + }, + ) + if err != nil { + return nil, err + } + return &core.Response[bool]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/noreqbody/client.go b/seed/go-sdk/go-deterministic-ordering/noreqbody/client.go new file mode 100644 index 000000000000..908411505592 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/noreqbody/client.go @@ -0,0 +1,61 @@ +// Code generated by Fern. DO NOT EDIT. + +package noreqbody + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) GetWithNoRequestBody( + ctx context.Context, + opts ...option.RequestOption, +) (*types.ObjectWithOptionalField, error) { + response, err := c.WithRawResponse.GetWithNoRequestBody( + ctx, + opts..., + ) + if err != nil { + return nil, err + } + return response.Body, nil +} + +func (c *Client) PostWithNoRequestBody( + ctx context.Context, + opts ...option.RequestOption, +) (string, error) { + response, err := c.WithRawResponse.PostWithNoRequestBody( + ctx, + opts..., + ) + if err != nil { + return "", err + } + return response.Body, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/noreqbody/no_req_body_test/no_req_body_test.go b/seed/go-sdk/go-deterministic-ordering/noreqbody/no_req_body_test/no_req_body_test.go new file mode 100644 index 000000000000..596a49fd4b07 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/noreqbody/no_req_body_test/no_req_body_test.go @@ -0,0 +1,104 @@ +// Code generated by Fern. DO NOT EDIT. + +package no_req_body_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestNoReqBodyGetWithNoRequestBodyWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.NoReqBody.GetWithNoRequestBody( + context.TODO(), + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestNoReqBodyGetWithNoRequestBodyWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestNoReqBodyGetWithNoRequestBodyWithWireMock", "GET", "/no-req-body", nil, 1) +} + +func TestNoReqBodyPostWithNoRequestBodyWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + _, invocationErr := client.NoReqBody.PostWithNoRequestBody( + context.TODO(), + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestNoReqBodyPostWithNoRequestBodyWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestNoReqBodyPostWithNoRequestBodyWithWireMock", "POST", "/no-req-body", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/noreqbody/raw_client.go b/seed/go-sdk/go-deterministic-ordering/noreqbody/raw_client.go new file mode 100644 index 000000000000..0b7a8c2b94df --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/noreqbody/raw_client.go @@ -0,0 +1,109 @@ +// Code generated by Fern. DO NOT EDIT. + +package noreqbody + +import ( + context "context" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + types "github.com/go-deterministic-ordering/fern/types" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetWithNoRequestBody( + ctx context.Context, + opts ...option.RequestOption, +) (*core.Response[*types.ObjectWithOptionalField], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/no-req-body" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response *types.ObjectWithOptionalField + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[*types.ObjectWithOptionalField]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) PostWithNoRequestBody( + ctx context.Context, + opts ...option.RequestOption, +) (*core.Response[string], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/no-req-body" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response string + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[string]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/option/request_option.go b/seed/go-sdk/go-deterministic-ordering/option/request_option.go new file mode 100644 index 000000000000..b9eb8133379e --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/option/request_option.go @@ -0,0 +1,80 @@ +// Code generated by Fern. DO NOT EDIT. + +package option + +import ( + core "github.com/go-deterministic-ordering/fern/core" + http "net/http" + url "net/url" +) + +// RequestOption adapts the behavior of an individual 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, + } +} + +// WithMaxStreamBufSize configures the maximum buffer size for streaming responses. +// This controls the maximum size of a single message (in bytes) that the stream +// can process. By default, this is set to 1MB. +func WithMaxStreamBufSize(size int) *core.MaxBufSizeOption { + return &core.MaxBufSizeOption{ + MaxBufSize: size, + } +} + +// WithToken sets the 'Authorization: Bearer ' request header. +func WithToken(token string) *core.TokenOption { + return &core.TokenOption{ + Token: token, + } +} diff --git a/seed/go-sdk/go-deterministic-ordering/pointer.go b/seed/go-sdk/go-deterministic-ordering/pointer.go new file mode 100644 index 000000000000..6a1e2698de9d --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/pointer.go @@ -0,0 +1,137 @@ +package exhaustive + +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 +} + +// Bytes returns a pointer to the given []byte value. +func Bytes(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/go-deterministic-ordering/pointer_test.go b/seed/go-sdk/go-deterministic-ordering/pointer_test.go new file mode 100644 index 000000000000..4463295f1669 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/pointer_test.go @@ -0,0 +1,211 @@ +package exhaustive + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestBool(t *testing.T) { + value := true + ptr := Bool(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestByte(t *testing.T) { + value := byte(42) + ptr := Byte(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestComplex64(t *testing.T) { + value := complex64(1 + 2i) + ptr := Complex64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestComplex128(t *testing.T) { + value := complex128(1 + 2i) + ptr := Complex128(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestFloat32(t *testing.T) { + value := float32(3.14) + ptr := Float32(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestFloat64(t *testing.T) { + value := 3.14159 + ptr := Float64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt(t *testing.T) { + value := 42 + ptr := Int(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt8(t *testing.T) { + value := int8(42) + ptr := Int8(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt16(t *testing.T) { + value := int16(42) + ptr := Int16(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt32(t *testing.T) { + value := int32(42) + ptr := Int32(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt64(t *testing.T) { + value := int64(42) + ptr := Int64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestRune(t *testing.T) { + value := 'A' + ptr := Rune(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestString(t *testing.T) { + value := "hello" + ptr := String(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint(t *testing.T) { + value := uint(42) + ptr := Uint(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint8(t *testing.T) { + value := uint8(42) + ptr := Uint8(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint16(t *testing.T) { + value := uint16(42) + ptr := Uint16(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint32(t *testing.T) { + value := uint32(42) + ptr := Uint32(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint64(t *testing.T) { + value := uint64(42) + ptr := Uint64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUintptr(t *testing.T) { + value := uintptr(42) + ptr := Uintptr(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUUID(t *testing.T) { + value := uuid.New() + ptr := UUID(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestTime(t *testing.T) { + value := time.Now() + ptr := Time(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestMustParseDate(t *testing.T) { + t.Run("valid date", func(t *testing.T) { + result := MustParseDate("2024-01-15") + expected, _ := time.Parse("2006-01-02", "2024-01-15") + assert.Equal(t, expected, result) + }) + + t.Run("invalid date panics", func(t *testing.T) { + assert.Panics(t, func() { + MustParseDate("invalid-date") + }) + }) +} + +func TestMustParseDateTime(t *testing.T) { + t.Run("valid datetime", func(t *testing.T) { + result := MustParseDateTime("2024-01-15T10:30:00Z") + expected, _ := time.Parse(time.RFC3339, "2024-01-15T10:30:00Z") + assert.Equal(t, expected, result) + }) + + t.Run("invalid datetime panics", func(t *testing.T) { + assert.Panics(t, func() { + MustParseDateTime("invalid-datetime") + }) + }) +} + +func TestPointerHelpersWithZeroValues(t *testing.T) { + t.Run("zero bool", func(t *testing.T) { + ptr := Bool(false) + assert.NotNil(t, ptr) + assert.Equal(t, false, *ptr) + }) + + t.Run("zero int", func(t *testing.T) { + ptr := Int(0) + assert.NotNil(t, ptr) + assert.Equal(t, 0, *ptr) + }) + + t.Run("empty string", func(t *testing.T) { + ptr := String("") + assert.NotNil(t, ptr) + assert.Equal(t, "", *ptr) + }) + + t.Run("zero time", func(t *testing.T) { + zeroTime := time.Time{} + ptr := Time(zeroTime) + assert.NotNil(t, ptr) + assert.Equal(t, zeroTime, *ptr) + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/reference.md b/seed/go-sdk/go-deterministic-ordering/reference.md new file mode 100644 index 000000000000..0bbbc6a3c40a --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/reference.md @@ -0,0 +1,3797 @@ +# Reference +## Endpoints Container +
client.Endpoints.Container.GetAndReturnListOfPrimitives(request) -> []string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := []string{ + "string", + "string", + } +client.Endpoints.Container.GetAndReturnListOfPrimitives( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `[]string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Container.GetAndReturnListOfObjects(request) -> []*types.ObjectWithRequiredField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := []*types.ObjectWithRequiredField{ + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } +client.Endpoints.Container.GetAndReturnListOfObjects( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `[]*types.ObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Container.GetAndReturnSetOfPrimitives(request) -> []string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := []string{ + "string", + } +client.Endpoints.Container.GetAndReturnSetOfPrimitives( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `[]string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Container.GetAndReturnSetOfObjects(request) -> []*types.ObjectWithRequiredField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := []*types.ObjectWithRequiredField{ + &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } +client.Endpoints.Container.GetAndReturnSetOfObjects( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `[]*types.ObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Container.GetAndReturnMapPrimToPrim(request) -> map[string]string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := map[string]string{ + "string": "string", + } +client.Endpoints.Container.GetAndReturnMapPrimToPrim( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `map[string]string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Container.GetAndReturnMapOfPrimToObject(request) -> map[string]*types.ObjectWithRequiredField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := map[string]*types.ObjectWithRequiredField{ + "string": &types.ObjectWithRequiredField{ + FieldString: "string", + }, + } +client.Endpoints.Container.GetAndReturnMapOfPrimToObject( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `map[string]*types.ObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Container.GetAndReturnMapOfPrimToUndiscriminatedUnion(request) -> map[string]*types.MixedType +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := map[string]*types.MixedType{ + "string": &types.MixedType{ + Double: 1.1, + }, + } +client.Endpoints.Container.GetAndReturnMapOfPrimToUndiscriminatedUnion( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `map[string]*types.MixedType` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Container.GetAndReturnOptional(request) -> *types.ObjectWithRequiredField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithRequiredField{ + FieldString: "string", + } +client.Endpoints.Container.GetAndReturnOptional( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +## Endpoints ContentType +
client.Endpoints.ContentType.PostJsonPatchContentType(request) -> error +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } +client.Endpoints.ContentType.PostJsonPatchContentType( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithOptionalField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.ContentType.PostJsonPatchContentWithCharsetType(request) -> error +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } +client.Endpoints.ContentType.PostJsonPatchContentWithCharsetType( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithOptionalField` + +
+
+
+
+ + +
+
+
+ +## Endpoints DuplicateNamesA +
client.Endpoints.DuplicateNamesA.Create(request) -> *types.ObjectWithOptionalField +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create endpoint for service A +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.CreateRequestA{ + Name: "name", + Value: 1, + } +client.Endpoints.DuplicateNamesA.Create( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `string` + +
+
+ +
+
+ +**value:** `int` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.DuplicateNamesA.Get(Id) -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get endpoint for service A +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.GetRequestA{ + Id: "id", + Filter: fern.String( + "filter", + ), + } +client.Endpoints.DuplicateNamesA.Get( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+ +
+
+ +**filter:** `*string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.DuplicateNamesA.List() -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List endpoint for service A +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.ListRequestA{ + Page: fern.Int( + 1, + ), + Limit: fern.Int( + 1, + ), + } +client.Endpoints.DuplicateNamesA.List( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**page:** `*int` + +
+
+ +
+
+ +**limit:** `*int` + +
+
+
+
+ + +
+
+
+ +## Endpoints DuplicateNamesB +
client.Endpoints.DuplicateNamesB.Create(request) -> *types.ObjectWithOptionalField +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create endpoint for service B +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.CreateRequestB{ + Description: "description", + Count: 1, + } +client.Endpoints.DuplicateNamesB.Create( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**description:** `string` + +
+
+ +
+
+ +**count:** `int` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.DuplicateNamesB.Get(Id) -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get endpoint for service B +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.GetRequestB{ + Id: "id", + Expand: fern.Bool( + true, + ), + } +client.Endpoints.DuplicateNamesB.Get( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+ +
+
+ +**expand:** `*bool` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.DuplicateNamesB.List() -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List endpoint for service B +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.ListRequestB{ + Cursor: fern.String( + "cursor", + ), + Size: fern.Int( + 1, + ), + } +client.Endpoints.DuplicateNamesB.List( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**cursor:** `*string` + +
+
+ +
+
+ +**size:** `*int` + +
+
+
+
+ + +
+
+
+ +## Endpoints DuplicateNamesC +
client.Endpoints.DuplicateNamesC.Create(request) -> *types.ObjectWithOptionalField +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create endpoint for service C +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.CreateRequestC{ + Label: "label", + Priority: 1, + } +client.Endpoints.DuplicateNamesC.Create( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**label:** `string` + +
+
+ +
+
+ +**priority:** `int` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.DuplicateNamesC.Get(Id) -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Get endpoint for service C +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.GetRequestC{ + Id: "id", + Verbose: fern.Bool( + true, + ), + } +client.Endpoints.DuplicateNamesC.Get( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+ +
+
+ +**verbose:** `*bool` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.DuplicateNamesC.List() -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List endpoint for service C +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.ListRequestC{ + Offset: fern.Int( + 1, + ), + Count: fern.Int( + 1, + ), + } +client.Endpoints.DuplicateNamesC.List( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**offset:** `*int` + +
+
+ +
+
+ +**count:** `*int` + +
+
+
+
+ + +
+
+
+ +## Endpoints Enum +
client.Endpoints.Enum.GetAndReturnEnum(request) -> *types.WeatherReport +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Enum.GetAndReturnEnum( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.WeatherReport` + +
+
+
+
+ + +
+
+
+ +## Endpoints HttpMethods +
client.Endpoints.HttpMethods.TestGet(Id) -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.HttpMethods.TestGet( + context.TODO(), + "id", + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.HttpMethods.TestPost(request) -> *types.ObjectWithOptionalField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithRequiredField{ + FieldString: "string", + } +client.Endpoints.HttpMethods.TestPost( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.HttpMethods.TestPut(Id, request) -> *types.ObjectWithOptionalField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithRequiredField{ + FieldString: "string", + } +client.Endpoints.HttpMethods.TestPut( + context.TODO(), + "id", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+ +
+
+ +**request:** `*types.ObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.HttpMethods.TestPatch(Id, request) -> *types.ObjectWithOptionalField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } +client.Endpoints.HttpMethods.TestPatch( + context.TODO(), + "id", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+ +
+
+ +**request:** `*types.ObjectWithOptionalField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.HttpMethods.TestDelete(Id) -> bool +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.HttpMethods.TestDelete( + context.TODO(), + "id", + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+
+
+ + +
+
+
+ +## Endpoints Object +
client.Endpoints.Object.GetAndReturnWithOptionalField(request) -> *types.ObjectWithOptionalField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + } +client.Endpoints.Object.GetAndReturnWithOptionalField( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithOptionalField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Object.GetAndReturnWithRequiredField(request) -> *types.ObjectWithRequiredField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithRequiredField{ + FieldString: "string", + } +client.Endpoints.Object.GetAndReturnWithRequiredField( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Object.GetAndReturnWithMapOfMap(request) -> *types.ObjectWithMapOfMap +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithMapOfMap{ + Map: map[string]map[string]string{ + "map": map[string]string{ + "map": "map", + }, + }, + } +client.Endpoints.Object.GetAndReturnWithMapOfMap( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithMapOfMap` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Object.GetAndReturnNestedWithOptionalField(request) -> *types.NestedObjectWithOptionalField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.NestedObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } +client.Endpoints.Object.GetAndReturnNestedWithOptionalField( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.NestedObjectWithOptionalField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Object.GetAndReturnNestedWithRequiredField(String, request) -> *types.NestedObjectWithRequiredField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } +client.Endpoints.Object.GetAndReturnNestedWithRequiredField( + context.TODO(), + "string", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**string_:** `string` + +
+
+ +
+
+ +**request:** `*types.NestedObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Object.GetAndReturnNestedWithRequiredFieldAsList(request) -> *types.NestedObjectWithRequiredField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := []*types.NestedObjectWithRequiredField{ + &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + }, + &types.NestedObjectWithRequiredField{ + FieldString: "string", + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + }, + } +client.Endpoints.Object.GetAndReturnNestedWithRequiredFieldAsList( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `[]*types.NestedObjectWithRequiredField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Object.GetAndReturnWithUnknownField(request) -> *types.ObjectWithUnknownField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithUnknownField{ + Unknown: map[string]any{ + "$ref": "https://example.com/schema", + }, + } +client.Endpoints.Object.GetAndReturnWithUnknownField( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithUnknownField` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Object.GetAndReturnWithDatetimeLikeString(request) -> *types.ObjectWithDatetimeLikeString +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Tests that string fields containing datetime-like values are NOT reformatted. +The datetimeLikeString field should preserve its exact value "2023-08-31T14:15:22Z" +without being converted to "2023-08-31T14:15:22.000Z". +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.ObjectWithDatetimeLikeString{ + DatetimeLikeString: "2023-08-31T14:15:22Z", + ActualDatetime: fern.MustParseDateTime( + "2023-08-31T14:15:22Z", + ), + } +client.Endpoints.Object.GetAndReturnWithDatetimeLikeString( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.ObjectWithDatetimeLikeString` + +
+
+
+
+ + +
+
+
+ +## Endpoints Pagination +
client.Endpoints.Pagination.ListItems() -> *endpoints.PaginatedResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List items with cursor pagination +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.ListItemsRequest{ + Cursor: fern.String( + "cursor", + ), + Limit: fern.Int( + 1, + ), + } +client.Endpoints.Pagination.ListItems( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**cursor:** `*string` — The cursor for pagination + +
+
+ +
+
+ +**limit:** `*int` — Maximum number of items to return + +
+
+
+
+ + +
+
+
+ +## Endpoints Params +
client.Endpoints.Params.GetWithPath(Param) -> string +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET with path param +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Params.GetWithPath( + context.TODO(), + "param", + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**param:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.GetWithInlinePath(Param) -> string +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET with path param +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Params.GetWithPath( + context.TODO(), + "param", + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**param:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.GetWithQuery() -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET with query param +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.GetWithQuery{ + Query: "query", + Number: 1, + } +client.Endpoints.Params.GetWithQuery( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**query:** `string` + +
+
+ +
+
+ +**number:** `int` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.GetWithAllowMultipleQuery() -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET with multiple of same query param +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.GetWithQuery{ + Query: "query", + Number: 1, + } +client.Endpoints.Params.GetWithQuery( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**query:** `string` + +
+
+ +
+
+ +**number:** `int` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.GetWithPathAndQuery(Param) -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET with path and query params +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.GetWithPathAndQuery{ + Query: "query", + } +client.Endpoints.Params.GetWithPathAndQuery( + context.TODO(), + "param", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**param:** `string` + +
+
+ +
+
+ +**query:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.GetWithInlinePathAndQuery(Param) -> error +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET with path and query params +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.GetWithPathAndQuery{ + Query: "query", + } +client.Endpoints.Params.GetWithPathAndQuery( + context.TODO(), + "param", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**param:** `string` + +
+
+ +
+
+ +**query:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.ModifyWithPath(Param, request) -> string +
+
+ +#### 📝 Description + +
+
+ +
+
+ +PUT to update with path param +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Params.ModifyWithPath( + context.TODO(), + "param", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**param:** `string` + +
+
+ +
+
+ +**request:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.ModifyWithInlinePath(Param, request) -> string +
+
+ +#### 📝 Description + +
+
+ +
+
+ +PUT to update with path param +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Params.ModifyWithPath( + context.TODO(), + "param", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**param:** `string` + +
+
+ +
+
+ +**request:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Params.UploadWithPath(Param, request) -> *types.ObjectWithRequiredField +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST bytes with path param returning object +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Params.UploadWithPath( + context.TODO(), + "upload-path", + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**param:** `string` + +
+
+
+
+ + +
+
+
+ +## Endpoints Primitive +
client.Endpoints.Primitive.GetAndReturnString(request) -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnString( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `string` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnInt(request) -> int +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnInt( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `int` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnLong(request) -> int64 +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnLong( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `int64` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnDouble(request) -> float64 +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnDouble( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `float64` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnBool(request) -> bool +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnBool( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `bool` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnDatetime(request) -> time.Time +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnDatetime( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `time.Time` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnDate(request) -> time.Time +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnDate( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `time.Time` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnUuid(request) -> uuid.UUID +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnUuid( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `uuid.UUID` + +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Primitive.GetAndReturnBase64(request) -> []byte +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Primitive.GetAndReturnBase64( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `[]byte` + +
+
+
+
+ + +
+
+
+ +## Endpoints Put +
client.Endpoints.Put.Add(Id) -> *endpoints.PutResponse +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.PutRequest{ + Id: "id", + } +client.Endpoints.Put.Add( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `string` + +
+
+
+
+ + +
+
+
+ +## Endpoints Union +
client.Endpoints.Union.GetAndReturnUnion(request) -> *types.Animal +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &types.Animal{ + Dog: &types.Dog{ + Name: "name", + LikesToWoof: true, + }, + } +client.Endpoints.Union.GetAndReturnUnion( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `*types.Animal` + +
+
+
+
+ + +
+
+
+ +## Endpoints Urls +
client.Endpoints.Urls.WithMixedCase() -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Urls.WithMixedCase( + context.TODO(), + ) +} +``` +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Urls.NoEndingSlash() -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Urls.NoEndingSlash( + context.TODO(), + ) +} +``` +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Urls.WithEndingSlash() -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Urls.WithEndingSlash( + context.TODO(), + ) +} +``` +
+
+
+
+ + +
+
+
+ +
client.Endpoints.Urls.WithUnderscores() -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.Endpoints.Urls.WithUnderscores( + context.TODO(), + ) +} +``` +
+
+
+
+ + +
+
+
+ +## InlinedRequests +
client.InlinedRequests.PostWithObjectBodyandResponse(request) -> *types.ObjectWithOptionalField +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST with custom object in request body, response is an object +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.PostWithObjectBody{ + FieldString: "string", + Integer: 1, + NestedObject: &types.ObjectWithOptionalField{ + FieldString: fern.String( + "string", + ), + Integer: fern.Int( + 1, + ), + Long: fern.Int64( + int64(1000000), + ), + Double: fern.Float64( + 1.1, + ), + Bool: fern.Bool( + true, + ), + Datetime: fern.Time( + fern.MustParseDateTime( + "2024-01-15T09:30:00Z", + ), + ), + Date: fern.Time( + fern.MustParseDate( + "2023-01-15", + ), + ), + Uuid: fern.UUID( + uuid.MustParse( + "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", + ), + ), + Base64: fern.Bytes( + []byte("SGVsbG8gd29ybGQh"), + ), + List: []string{ + "list", + "list", + }, + Set: []string{ + "set", + }, + Map: map[int]string{ + 1: "map", + }, + Bigint: fern.String( + "1000000", + ), + }, + } +client.InlinedRequests.PostWithObjectBodyandResponse( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**string_:** `string` + +
+
+ +
+
+ +**integer:** `int` + +
+
+ +
+
+ +**nestedObject:** `*types.ObjectWithOptionalField` + +
+
+
+
+ + +
+
+
+ +## NoAuth +
client.NoAuth.PostWithNoAuth(request) -> bool +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST request with no auth +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := map[string]any{ + "key": "value", + } +client.NoAuth.PostWithNoAuth( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `any` + +
+
+
+
+ + +
+
+
+ +## NoReqBody +
client.NoReqBody.GetWithNoRequestBody() -> *types.ObjectWithOptionalField +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.NoReqBody.GetWithNoRequestBody( + context.TODO(), + ) +} +``` +
+
+
+
+ + +
+
+
+ +
client.NoReqBody.PostWithNoRequestBody() -> string +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.NoReqBody.PostWithNoRequestBody( + context.TODO(), + ) +} +``` +
+
+
+
+ + +
+
+
+ +## ReqWithHeaders +
client.ReqWithHeaders.GetWithCustomHeader(request) -> error +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := &fern.ReqWithHeaders{ + XTestServiceHeader: "X-TEST-SERVICE-HEADER", + XTestEndpointHeader: "X-TEST-ENDPOINT-HEADER", + Body: "string", + } +client.ReqWithHeaders.GetWithCustomHeader( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**xTestEndpointHeader:** `string` + +
+
+ +
+
+ +**request:** `string` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/go-sdk/go-deterministic-ordering/requests.go b/seed/go-sdk/go-deterministic-ordering/requests.go new file mode 100644 index 000000000000..34dcf7eebe4c --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/requests.go @@ -0,0 +1,743 @@ +// Code generated by Fern. DO NOT EDIT. + +package exhaustive + +import ( + json "encoding/json" + internal "github.com/go-deterministic-ordering/fern/internal" + types "github.com/go-deterministic-ordering/fern/types" + big "math/big" +) + +var ( + putRequestFieldId = big.NewInt(1 << 0) +) + +type PutRequest struct { + Id string `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (p *PutRequest) require(field *big.Int) { + if p.explicitFields == nil { + p.explicitFields = big.NewInt(0) + } + p.explicitFields.Or(p.explicitFields, field) +} + +// SetId sets the Id field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (p *PutRequest) SetId(id string) { + p.Id = id + p.require(putRequestFieldId) +} + +var ( + createRequestBFieldDescription = big.NewInt(1 << 0) + createRequestBFieldCount = big.NewInt(1 << 1) +) + +type CreateRequestB struct { + Description string `json:"description" url:"-"` + Count int `json:"count" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (c *CreateRequestB) require(field *big.Int) { + if c.explicitFields == nil { + c.explicitFields = big.NewInt(0) + } + c.explicitFields.Or(c.explicitFields, field) +} + +// SetDescription sets the Description field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *CreateRequestB) SetDescription(description string) { + c.Description = description + c.require(createRequestBFieldDescription) +} + +// SetCount sets the Count field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *CreateRequestB) SetCount(count int) { + c.Count = count + c.require(createRequestBFieldCount) +} + +func (c *CreateRequestB) UnmarshalJSON(data []byte) error { + type unmarshaler CreateRequestB + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { + return err + } + *c = CreateRequestB(body) + return nil +} + +func (c *CreateRequestB) MarshalJSON() ([]byte, error) { + type embed CreateRequestB + var marshaler = struct { + embed + }{ + embed: embed(*c), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, c.explicitFields) + return json.Marshal(explicitMarshaler) +} + +var ( + createRequestAFieldName = big.NewInt(1 << 0) + createRequestAFieldValue = big.NewInt(1 << 1) +) + +type CreateRequestA struct { + Name string `json:"name" url:"-"` + Value int `json:"value" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (c *CreateRequestA) require(field *big.Int) { + if c.explicitFields == nil { + c.explicitFields = big.NewInt(0) + } + c.explicitFields.Or(c.explicitFields, field) +} + +// SetName sets the Name field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *CreateRequestA) SetName(name string) { + c.Name = name + c.require(createRequestAFieldName) +} + +// SetValue sets the Value field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *CreateRequestA) SetValue(value int) { + c.Value = value + c.require(createRequestAFieldValue) +} + +func (c *CreateRequestA) UnmarshalJSON(data []byte) error { + type unmarshaler CreateRequestA + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { + return err + } + *c = CreateRequestA(body) + return nil +} + +func (c *CreateRequestA) MarshalJSON() ([]byte, error) { + type embed CreateRequestA + var marshaler = struct { + embed + }{ + embed: embed(*c), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, c.explicitFields) + return json.Marshal(explicitMarshaler) +} + +var ( + createRequestCFieldLabel = big.NewInt(1 << 0) + createRequestCFieldPriority = big.NewInt(1 << 1) +) + +type CreateRequestC struct { + Label string `json:"label" url:"-"` + Priority int `json:"priority" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (c *CreateRequestC) require(field *big.Int) { + if c.explicitFields == nil { + c.explicitFields = big.NewInt(0) + } + c.explicitFields.Or(c.explicitFields, field) +} + +// SetLabel sets the Label field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *CreateRequestC) SetLabel(label string) { + c.Label = label + c.require(createRequestCFieldLabel) +} + +// SetPriority sets the Priority field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *CreateRequestC) SetPriority(priority int) { + c.Priority = priority + c.require(createRequestCFieldPriority) +} + +func (c *CreateRequestC) UnmarshalJSON(data []byte) error { + type unmarshaler CreateRequestC + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { + return err + } + *c = CreateRequestC(body) + return nil +} + +func (c *CreateRequestC) MarshalJSON() ([]byte, error) { + type embed CreateRequestC + var marshaler = struct { + embed + }{ + embed: embed(*c), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, c.explicitFields) + return json.Marshal(explicitMarshaler) +} + +var ( + getRequestCFieldId = big.NewInt(1 << 0) + getRequestCFieldVerbose = big.NewInt(1 << 1) +) + +type GetRequestC struct { + Id string `json:"-" url:"-"` + Verbose *bool `json:"-" url:"verbose,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetRequestC) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetId sets the Id field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetRequestC) SetId(id string) { + g.Id = id + g.require(getRequestCFieldId) +} + +// SetVerbose sets the Verbose field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetRequestC) SetVerbose(verbose *bool) { + g.Verbose = verbose + g.require(getRequestCFieldVerbose) +} + +var ( + getRequestAFieldId = big.NewInt(1 << 0) + getRequestAFieldFilter = big.NewInt(1 << 1) +) + +type GetRequestA struct { + Id string `json:"-" url:"-"` + Filter *string `json:"-" url:"filter,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetRequestA) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetId sets the Id field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetRequestA) SetId(id string) { + g.Id = id + g.require(getRequestAFieldId) +} + +// SetFilter sets the Filter field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetRequestA) SetFilter(filter *string) { + g.Filter = filter + g.require(getRequestAFieldFilter) +} + +var ( + getRequestBFieldId = big.NewInt(1 << 0) + getRequestBFieldExpand = big.NewInt(1 << 1) +) + +type GetRequestB struct { + Id string `json:"-" url:"-"` + Expand *bool `json:"-" url:"expand,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetRequestB) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetId sets the Id field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetRequestB) SetId(id string) { + g.Id = id + g.require(getRequestBFieldId) +} + +// SetExpand sets the Expand field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetRequestB) SetExpand(expand *bool) { + g.Expand = expand + g.require(getRequestBFieldExpand) +} + +var ( + getWithMultipleQueryFieldQuery = big.NewInt(1 << 0) + getWithMultipleQueryFieldNumber = big.NewInt(1 << 1) +) + +type GetWithMultipleQuery struct { + Query []string `json:"-" url:"query"` + Number []int `json:"-" url:"number"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetWithMultipleQuery) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetQuery sets the Query field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithMultipleQuery) SetQuery(query []string) { + g.Query = query + g.require(getWithMultipleQueryFieldQuery) +} + +// SetNumber sets the Number field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithMultipleQuery) SetNumber(number []int) { + g.Number = number + g.require(getWithMultipleQueryFieldNumber) +} + +var ( + reqWithHeadersFieldXTestServiceHeader = big.NewInt(1 << 0) + reqWithHeadersFieldXTestEndpointHeader = big.NewInt(1 << 1) +) + +type ReqWithHeaders struct { + XTestServiceHeader string `json:"-" url:"-"` + XTestEndpointHeader string `json:"-" url:"-"` + Body string `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (r *ReqWithHeaders) require(field *big.Int) { + if r.explicitFields == nil { + r.explicitFields = big.NewInt(0) + } + r.explicitFields.Or(r.explicitFields, field) +} + +// SetXTestServiceHeader sets the XTestServiceHeader field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (r *ReqWithHeaders) SetXTestServiceHeader(xTestServiceHeader string) { + r.XTestServiceHeader = xTestServiceHeader + r.require(reqWithHeadersFieldXTestServiceHeader) +} + +// SetXTestEndpointHeader sets the XTestEndpointHeader field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (r *ReqWithHeaders) SetXTestEndpointHeader(xTestEndpointHeader string) { + r.XTestEndpointHeader = xTestEndpointHeader + r.require(reqWithHeadersFieldXTestEndpointHeader) +} + +func (r *ReqWithHeaders) UnmarshalJSON(data []byte) error { + var body string + if err := json.Unmarshal(data, &body); err != nil { + return err + } + r.Body = body + return nil +} + +func (r *ReqWithHeaders) MarshalJSON() ([]byte, error) { + return json.Marshal(r.Body) +} + +var ( + getWithInlinePathFieldParam = big.NewInt(1 << 0) +) + +type GetWithInlinePath struct { + Param string `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetWithInlinePath) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetParam sets the Param field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithInlinePath) SetParam(param string) { + g.Param = param + g.require(getWithInlinePathFieldParam) +} + +var ( + getWithInlinePathAndQueryFieldParam = big.NewInt(1 << 0) + getWithInlinePathAndQueryFieldQuery = big.NewInt(1 << 1) +) + +type GetWithInlinePathAndQuery struct { + Param string `json:"-" url:"-"` + Query string `json:"-" url:"query"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetWithInlinePathAndQuery) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetParam sets the Param field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithInlinePathAndQuery) SetParam(param string) { + g.Param = param + g.require(getWithInlinePathAndQueryFieldParam) +} + +// SetQuery sets the Query field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithInlinePathAndQuery) SetQuery(query string) { + g.Query = query + g.require(getWithInlinePathAndQueryFieldQuery) +} + +var ( + getWithPathAndQueryFieldQuery = big.NewInt(1 << 0) +) + +type GetWithPathAndQuery struct { + Query string `json:"-" url:"query"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetWithPathAndQuery) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetQuery sets the Query field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithPathAndQuery) SetQuery(query string) { + g.Query = query + g.require(getWithPathAndQueryFieldQuery) +} + +var ( + getWithQueryFieldQuery = big.NewInt(1 << 0) + getWithQueryFieldNumber = big.NewInt(1 << 1) +) + +type GetWithQuery struct { + Query string `json:"-" url:"query"` + Number int `json:"-" url:"number"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (g *GetWithQuery) require(field *big.Int) { + if g.explicitFields == nil { + g.explicitFields = big.NewInt(0) + } + g.explicitFields.Or(g.explicitFields, field) +} + +// SetQuery sets the Query field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithQuery) SetQuery(query string) { + g.Query = query + g.require(getWithQueryFieldQuery) +} + +// SetNumber sets the Number field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (g *GetWithQuery) SetNumber(number int) { + g.Number = number + g.require(getWithQueryFieldNumber) +} + +var ( + listRequestCFieldOffset = big.NewInt(1 << 0) + listRequestCFieldCount = big.NewInt(1 << 1) +) + +type ListRequestC struct { + Offset *int `json:"-" url:"offset,omitempty"` + Count *int `json:"-" url:"count,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (l *ListRequestC) require(field *big.Int) { + if l.explicitFields == nil { + l.explicitFields = big.NewInt(0) + } + l.explicitFields.Or(l.explicitFields, field) +} + +// SetOffset sets the Offset field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListRequestC) SetOffset(offset *int) { + l.Offset = offset + l.require(listRequestCFieldOffset) +} + +// SetCount sets the Count field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListRequestC) SetCount(count *int) { + l.Count = count + l.require(listRequestCFieldCount) +} + +var ( + listRequestBFieldCursor = big.NewInt(1 << 0) + listRequestBFieldSize = big.NewInt(1 << 1) +) + +type ListRequestB struct { + Cursor *string `json:"-" url:"cursor,omitempty"` + Size *int `json:"-" url:"size,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (l *ListRequestB) require(field *big.Int) { + if l.explicitFields == nil { + l.explicitFields = big.NewInt(0) + } + l.explicitFields.Or(l.explicitFields, field) +} + +// SetCursor sets the Cursor field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListRequestB) SetCursor(cursor *string) { + l.Cursor = cursor + l.require(listRequestBFieldCursor) +} + +// SetSize sets the Size field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListRequestB) SetSize(size *int) { + l.Size = size + l.require(listRequestBFieldSize) +} + +var ( + listRequestAFieldPage = big.NewInt(1 << 0) + listRequestAFieldLimit = big.NewInt(1 << 1) +) + +type ListRequestA struct { + Page *int `json:"-" url:"page,omitempty"` + Limit *int `json:"-" url:"limit,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (l *ListRequestA) require(field *big.Int) { + if l.explicitFields == nil { + l.explicitFields = big.NewInt(0) + } + l.explicitFields.Or(l.explicitFields, field) +} + +// SetPage sets the Page field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListRequestA) SetPage(page *int) { + l.Page = page + l.require(listRequestAFieldPage) +} + +// SetLimit sets the Limit field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListRequestA) SetLimit(limit *int) { + l.Limit = limit + l.require(listRequestAFieldLimit) +} + +var ( + listItemsRequestFieldCursor = big.NewInt(1 << 0) + listItemsRequestFieldLimit = big.NewInt(1 << 1) +) + +type ListItemsRequest struct { + // The cursor for pagination + Cursor *string `json:"-" url:"cursor,omitempty"` + // Maximum number of items to return + Limit *int `json:"-" url:"limit,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (l *ListItemsRequest) require(field *big.Int) { + if l.explicitFields == nil { + l.explicitFields = big.NewInt(0) + } + l.explicitFields.Or(l.explicitFields, field) +} + +// SetCursor sets the Cursor field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListItemsRequest) SetCursor(cursor *string) { + l.Cursor = cursor + l.require(listItemsRequestFieldCursor) +} + +// SetLimit sets the Limit field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (l *ListItemsRequest) SetLimit(limit *int) { + l.Limit = limit + l.require(listItemsRequestFieldLimit) +} + +var ( + modifyResourceAtInlinedPathFieldParam = big.NewInt(1 << 0) +) + +type ModifyResourceAtInlinedPath struct { + Param string `json:"-" url:"-"` + Body string `json:"-" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (m *ModifyResourceAtInlinedPath) require(field *big.Int) { + if m.explicitFields == nil { + m.explicitFields = big.NewInt(0) + } + m.explicitFields.Or(m.explicitFields, field) +} + +// SetParam sets the Param field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (m *ModifyResourceAtInlinedPath) SetParam(param string) { + m.Param = param + m.require(modifyResourceAtInlinedPathFieldParam) +} + +func (m *ModifyResourceAtInlinedPath) UnmarshalJSON(data []byte) error { + var body string + if err := json.Unmarshal(data, &body); err != nil { + return err + } + m.Body = body + return nil +} + +func (m *ModifyResourceAtInlinedPath) MarshalJSON() ([]byte, error) { + return json.Marshal(m.Body) +} + +var ( + postWithObjectBodyFieldFieldString = big.NewInt(1 << 0) + postWithObjectBodyFieldInteger = big.NewInt(1 << 1) + postWithObjectBodyFieldNestedObject = big.NewInt(1 << 2) +) + +type PostWithObjectBody struct { + FieldString string `json:"string" url:"-"` + Integer int `json:"integer" url:"-"` + NestedObject *types.ObjectWithOptionalField `json:"NestedObject" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +func (p *PostWithObjectBody) require(field *big.Int) { + if p.explicitFields == nil { + p.explicitFields = big.NewInt(0) + } + p.explicitFields.Or(p.explicitFields, field) +} + +// SetFieldString sets the FieldString field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (p *PostWithObjectBody) SetFieldString(string_ string) { + p.FieldString = string_ + p.require(postWithObjectBodyFieldFieldString) +} + +// SetInteger sets the Integer field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (p *PostWithObjectBody) SetInteger(integer int) { + p.Integer = integer + p.require(postWithObjectBodyFieldInteger) +} + +// SetNestedObject sets the NestedObject field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (p *PostWithObjectBody) SetNestedObject(nestedObject *types.ObjectWithOptionalField) { + p.NestedObject = nestedObject + p.require(postWithObjectBodyFieldNestedObject) +} + +func (p *PostWithObjectBody) UnmarshalJSON(data []byte) error { + type unmarshaler PostWithObjectBody + var body unmarshaler + if err := json.Unmarshal(data, &body); err != nil { + return err + } + *p = PostWithObjectBody(body) + return nil +} + +func (p *PostWithObjectBody) MarshalJSON() ([]byte, error) { + type embed PostWithObjectBody + var marshaler = struct { + embed + }{ + embed: embed(*p), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, p.explicitFields) + return json.Marshal(explicitMarshaler) +} diff --git a/seed/go-sdk/go-deterministic-ordering/requests_test.go b/seed/go-sdk/go-deterministic-ordering/requests_test.go new file mode 100644 index 000000000000..b59758eea142 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/requests_test.go @@ -0,0 +1,1490 @@ +// Code generated by Fern. DO NOT EDIT. + +package exhaustive + +import ( + json "encoding/json" + types "github.com/go-deterministic-ordering/fern/types" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" +) + +func TestSettersPutRequest(t *testing.T) { + t.Run("SetId", func(t *testing.T) { + obj := &PutRequest{} + var fernTestValueId string + obj.SetId(fernTestValueId) + assert.Equal(t, fernTestValueId, obj.Id) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitPutRequest(t *testing.T) { + t.Run("SetId_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PutRequest{} + var fernTestValueId string + + // Act + obj.SetId(fernTestValueId) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersCreateRequestB(t *testing.T) { + t.Run("SetDescription", func(t *testing.T) { + obj := &CreateRequestB{} + var fernTestValueDescription string + obj.SetDescription(fernTestValueDescription) + assert.Equal(t, fernTestValueDescription, obj.Description) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetCount", func(t *testing.T) { + obj := &CreateRequestB{} + var fernTestValueCount int + obj.SetCount(fernTestValueCount) + assert.Equal(t, fernTestValueCount, obj.Count) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitCreateRequestB(t *testing.T) { + t.Run("SetDescription_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &CreateRequestB{} + var fernTestValueDescription string + + // Act + obj.SetDescription(fernTestValueDescription) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetCount_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &CreateRequestB{} + var fernTestValueCount int + + // Act + obj.SetCount(fernTestValueCount) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersCreateRequestA(t *testing.T) { + t.Run("SetName", func(t *testing.T) { + obj := &CreateRequestA{} + var fernTestValueName string + obj.SetName(fernTestValueName) + assert.Equal(t, fernTestValueName, obj.Name) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetValue", func(t *testing.T) { + obj := &CreateRequestA{} + var fernTestValueValue int + obj.SetValue(fernTestValueValue) + assert.Equal(t, fernTestValueValue, obj.Value) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitCreateRequestA(t *testing.T) { + t.Run("SetName_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &CreateRequestA{} + var fernTestValueName string + + // Act + obj.SetName(fernTestValueName) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetValue_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &CreateRequestA{} + var fernTestValueValue int + + // Act + obj.SetValue(fernTestValueValue) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersCreateRequestC(t *testing.T) { + t.Run("SetLabel", func(t *testing.T) { + obj := &CreateRequestC{} + var fernTestValueLabel string + obj.SetLabel(fernTestValueLabel) + assert.Equal(t, fernTestValueLabel, obj.Label) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetPriority", func(t *testing.T) { + obj := &CreateRequestC{} + var fernTestValuePriority int + obj.SetPriority(fernTestValuePriority) + assert.Equal(t, fernTestValuePriority, obj.Priority) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitCreateRequestC(t *testing.T) { + t.Run("SetLabel_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &CreateRequestC{} + var fernTestValueLabel string + + // Act + obj.SetLabel(fernTestValueLabel) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetPriority_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &CreateRequestC{} + var fernTestValuePriority int + + // Act + obj.SetPriority(fernTestValuePriority) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetRequestC(t *testing.T) { + t.Run("SetId", func(t *testing.T) { + obj := &GetRequestC{} + var fernTestValueId string + obj.SetId(fernTestValueId) + assert.Equal(t, fernTestValueId, obj.Id) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetVerbose", func(t *testing.T) { + obj := &GetRequestC{} + var fernTestValueVerbose *bool + obj.SetVerbose(fernTestValueVerbose) + assert.Equal(t, fernTestValueVerbose, obj.Verbose) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetRequestC(t *testing.T) { + t.Run("SetId_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetRequestC{} + var fernTestValueId string + + // Act + obj.SetId(fernTestValueId) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetVerbose_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetRequestC{} + var fernTestValueVerbose *bool + + // Act + obj.SetVerbose(fernTestValueVerbose) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetRequestA(t *testing.T) { + t.Run("SetId", func(t *testing.T) { + obj := &GetRequestA{} + var fernTestValueId string + obj.SetId(fernTestValueId) + assert.Equal(t, fernTestValueId, obj.Id) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetFilter", func(t *testing.T) { + obj := &GetRequestA{} + var fernTestValueFilter *string + obj.SetFilter(fernTestValueFilter) + assert.Equal(t, fernTestValueFilter, obj.Filter) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetRequestA(t *testing.T) { + t.Run("SetId_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetRequestA{} + var fernTestValueId string + + // Act + obj.SetId(fernTestValueId) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetFilter_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetRequestA{} + var fernTestValueFilter *string + + // Act + obj.SetFilter(fernTestValueFilter) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetRequestB(t *testing.T) { + t.Run("SetId", func(t *testing.T) { + obj := &GetRequestB{} + var fernTestValueId string + obj.SetId(fernTestValueId) + assert.Equal(t, fernTestValueId, obj.Id) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetExpand", func(t *testing.T) { + obj := &GetRequestB{} + var fernTestValueExpand *bool + obj.SetExpand(fernTestValueExpand) + assert.Equal(t, fernTestValueExpand, obj.Expand) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetRequestB(t *testing.T) { + t.Run("SetId_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetRequestB{} + var fernTestValueId string + + // Act + obj.SetId(fernTestValueId) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetExpand_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetRequestB{} + var fernTestValueExpand *bool + + // Act + obj.SetExpand(fernTestValueExpand) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetWithMultipleQuery(t *testing.T) { + t.Run("SetQuery", func(t *testing.T) { + obj := &GetWithMultipleQuery{} + var fernTestValueQuery []string + obj.SetQuery(fernTestValueQuery) + assert.Equal(t, fernTestValueQuery, obj.Query) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetNumber", func(t *testing.T) { + obj := &GetWithMultipleQuery{} + var fernTestValueNumber []int + obj.SetNumber(fernTestValueNumber) + assert.Equal(t, fernTestValueNumber, obj.Number) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetWithMultipleQuery(t *testing.T) { + t.Run("SetQuery_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithMultipleQuery{} + var fernTestValueQuery []string + + // Act + obj.SetQuery(fernTestValueQuery) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetNumber_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithMultipleQuery{} + var fernTestValueNumber []int + + // Act + obj.SetNumber(fernTestValueNumber) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersReqWithHeaders(t *testing.T) { + t.Run("SetXTestServiceHeader", func(t *testing.T) { + obj := &ReqWithHeaders{} + var fernTestValueXTestServiceHeader string + obj.SetXTestServiceHeader(fernTestValueXTestServiceHeader) + assert.Equal(t, fernTestValueXTestServiceHeader, obj.XTestServiceHeader) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetXTestEndpointHeader", func(t *testing.T) { + obj := &ReqWithHeaders{} + var fernTestValueXTestEndpointHeader string + obj.SetXTestEndpointHeader(fernTestValueXTestEndpointHeader) + assert.Equal(t, fernTestValueXTestEndpointHeader, obj.XTestEndpointHeader) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitReqWithHeaders(t *testing.T) { + t.Run("SetXTestServiceHeader_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ReqWithHeaders{} + var fernTestValueXTestServiceHeader string + + // Act + obj.SetXTestServiceHeader(fernTestValueXTestServiceHeader) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetXTestEndpointHeader_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ReqWithHeaders{} + var fernTestValueXTestEndpointHeader string + + // Act + obj.SetXTestEndpointHeader(fernTestValueXTestEndpointHeader) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetWithInlinePath(t *testing.T) { + t.Run("SetParam", func(t *testing.T) { + obj := &GetWithInlinePath{} + var fernTestValueParam string + obj.SetParam(fernTestValueParam) + assert.Equal(t, fernTestValueParam, obj.Param) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetWithInlinePath(t *testing.T) { + t.Run("SetParam_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithInlinePath{} + var fernTestValueParam string + + // Act + obj.SetParam(fernTestValueParam) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetWithInlinePathAndQuery(t *testing.T) { + t.Run("SetParam", func(t *testing.T) { + obj := &GetWithInlinePathAndQuery{} + var fernTestValueParam string + obj.SetParam(fernTestValueParam) + assert.Equal(t, fernTestValueParam, obj.Param) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetQuery", func(t *testing.T) { + obj := &GetWithInlinePathAndQuery{} + var fernTestValueQuery string + obj.SetQuery(fernTestValueQuery) + assert.Equal(t, fernTestValueQuery, obj.Query) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetWithInlinePathAndQuery(t *testing.T) { + t.Run("SetParam_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithInlinePathAndQuery{} + var fernTestValueParam string + + // Act + obj.SetParam(fernTestValueParam) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetQuery_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithInlinePathAndQuery{} + var fernTestValueQuery string + + // Act + obj.SetQuery(fernTestValueQuery) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetWithPathAndQuery(t *testing.T) { + t.Run("SetQuery", func(t *testing.T) { + obj := &GetWithPathAndQuery{} + var fernTestValueQuery string + obj.SetQuery(fernTestValueQuery) + assert.Equal(t, fernTestValueQuery, obj.Query) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetWithPathAndQuery(t *testing.T) { + t.Run("SetQuery_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithPathAndQuery{} + var fernTestValueQuery string + + // Act + obj.SetQuery(fernTestValueQuery) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersGetWithQuery(t *testing.T) { + t.Run("SetQuery", func(t *testing.T) { + obj := &GetWithQuery{} + var fernTestValueQuery string + obj.SetQuery(fernTestValueQuery) + assert.Equal(t, fernTestValueQuery, obj.Query) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetNumber", func(t *testing.T) { + obj := &GetWithQuery{} + var fernTestValueNumber int + obj.SetNumber(fernTestValueNumber) + assert.Equal(t, fernTestValueNumber, obj.Number) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitGetWithQuery(t *testing.T) { + t.Run("SetQuery_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithQuery{} + var fernTestValueQuery string + + // Act + obj.SetQuery(fernTestValueQuery) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetNumber_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &GetWithQuery{} + var fernTestValueNumber int + + // Act + obj.SetNumber(fernTestValueNumber) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersListRequestC(t *testing.T) { + t.Run("SetOffset", func(t *testing.T) { + obj := &ListRequestC{} + var fernTestValueOffset *int + obj.SetOffset(fernTestValueOffset) + assert.Equal(t, fernTestValueOffset, obj.Offset) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetCount", func(t *testing.T) { + obj := &ListRequestC{} + var fernTestValueCount *int + obj.SetCount(fernTestValueCount) + assert.Equal(t, fernTestValueCount, obj.Count) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitListRequestC(t *testing.T) { + t.Run("SetOffset_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListRequestC{} + var fernTestValueOffset *int + + // Act + obj.SetOffset(fernTestValueOffset) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetCount_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListRequestC{} + var fernTestValueCount *int + + // Act + obj.SetCount(fernTestValueCount) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersListRequestB(t *testing.T) { + t.Run("SetCursor", func(t *testing.T) { + obj := &ListRequestB{} + var fernTestValueCursor *string + obj.SetCursor(fernTestValueCursor) + assert.Equal(t, fernTestValueCursor, obj.Cursor) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetSize", func(t *testing.T) { + obj := &ListRequestB{} + var fernTestValueSize *int + obj.SetSize(fernTestValueSize) + assert.Equal(t, fernTestValueSize, obj.Size) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitListRequestB(t *testing.T) { + t.Run("SetCursor_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListRequestB{} + var fernTestValueCursor *string + + // Act + obj.SetCursor(fernTestValueCursor) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetSize_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListRequestB{} + var fernTestValueSize *int + + // Act + obj.SetSize(fernTestValueSize) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersListRequestA(t *testing.T) { + t.Run("SetPage", func(t *testing.T) { + obj := &ListRequestA{} + var fernTestValuePage *int + obj.SetPage(fernTestValuePage) + assert.Equal(t, fernTestValuePage, obj.Page) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetLimit", func(t *testing.T) { + obj := &ListRequestA{} + var fernTestValueLimit *int + obj.SetLimit(fernTestValueLimit) + assert.Equal(t, fernTestValueLimit, obj.Limit) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitListRequestA(t *testing.T) { + t.Run("SetPage_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListRequestA{} + var fernTestValuePage *int + + // Act + obj.SetPage(fernTestValuePage) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetLimit_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListRequestA{} + var fernTestValueLimit *int + + // Act + obj.SetLimit(fernTestValueLimit) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersListItemsRequest(t *testing.T) { + t.Run("SetCursor", func(t *testing.T) { + obj := &ListItemsRequest{} + var fernTestValueCursor *string + obj.SetCursor(fernTestValueCursor) + assert.Equal(t, fernTestValueCursor, obj.Cursor) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetLimit", func(t *testing.T) { + obj := &ListItemsRequest{} + var fernTestValueLimit *int + obj.SetLimit(fernTestValueLimit) + assert.Equal(t, fernTestValueLimit, obj.Limit) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitListItemsRequest(t *testing.T) { + t.Run("SetCursor_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListItemsRequest{} + var fernTestValueCursor *string + + // Act + obj.SetCursor(fernTestValueCursor) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetLimit_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ListItemsRequest{} + var fernTestValueLimit *int + + // Act + obj.SetLimit(fernTestValueLimit) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersModifyResourceAtInlinedPath(t *testing.T) { + t.Run("SetParam", func(t *testing.T) { + obj := &ModifyResourceAtInlinedPath{} + var fernTestValueParam string + obj.SetParam(fernTestValueParam) + assert.Equal(t, fernTestValueParam, obj.Param) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitModifyResourceAtInlinedPath(t *testing.T) { + t.Run("SetParam_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ModifyResourceAtInlinedPath{} + var fernTestValueParam string + + // Act + obj.SetParam(fernTestValueParam) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersPostWithObjectBody(t *testing.T) { + t.Run("SetFieldString", func(t *testing.T) { + obj := &PostWithObjectBody{} + var fernTestValueFieldString string + obj.SetFieldString(fernTestValueFieldString) + assert.Equal(t, fernTestValueFieldString, obj.FieldString) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetInteger", func(t *testing.T) { + obj := &PostWithObjectBody{} + var fernTestValueInteger int + obj.SetInteger(fernTestValueInteger) + assert.Equal(t, fernTestValueInteger, obj.Integer) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetNestedObject", func(t *testing.T) { + obj := &PostWithObjectBody{} + var fernTestValueNestedObject *types.ObjectWithOptionalField + obj.SetNestedObject(fernTestValueNestedObject) + assert.Equal(t, fernTestValueNestedObject, obj.NestedObject) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestSettersMarkExplicitPostWithObjectBody(t *testing.T) { + t.Run("SetFieldString_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PostWithObjectBody{} + var fernTestValueFieldString string + + // Act + obj.SetFieldString(fernTestValueFieldString) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetInteger_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PostWithObjectBody{} + var fernTestValueInteger int + + // Act + obj.SetInteger(fernTestValueInteger) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetNestedObject_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &PostWithObjectBody{} + var fernTestValueNestedObject *types.ObjectWithOptionalField + + // Act + obj.SetNestedObject(fernTestValueNestedObject) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} diff --git a/seed/go-sdk/go-deterministic-ordering/reqwithheaders/client.go b/seed/go-sdk/go-deterministic-ordering/reqwithheaders/client.go new file mode 100644 index 000000000000..210f773049ca --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/reqwithheaders/client.go @@ -0,0 +1,49 @@ +// Code generated by Fern. DO NOT EDIT. + +package reqwithheaders + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (c *Client) GetWithCustomHeader( + ctx context.Context, + request *fern.ReqWithHeaders, + opts ...option.RequestOption, +) error { + _, err := c.WithRawResponse.GetWithCustomHeader( + ctx, + request, + opts..., + ) + if err != nil { + return err + } + return nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/reqwithheaders/raw_client.go b/seed/go-sdk/go-deterministic-ordering/reqwithheaders/raw_client.go new file mode 100644 index 000000000000..f493ae54014b --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/reqwithheaders/raw_client.go @@ -0,0 +1,71 @@ +// Code generated by Fern. DO NOT EDIT. + +package reqwithheaders + +import ( + context "context" + fern "github.com/go-deterministic-ordering/fern" + core "github.com/go-deterministic-ordering/fern/core" + internal "github.com/go-deterministic-ordering/fern/internal" + option "github.com/go-deterministic-ordering/fern/option" + http "net/http" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetWithCustomHeader( + ctx context.Context, + request *fern.ReqWithHeaders, + opts ...option.RequestOption, +) (*core.Response[any], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/test-headers/custom-header" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + headers.Add("X-TEST-ENDPOINT-HEADER", request.XTestEndpointHeader) + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + }, + ) + if err != nil { + return nil, err + } + return &core.Response[any]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: nil, + }, nil +} diff --git a/seed/go-sdk/go-deterministic-ordering/reqwithheaders/req_with_headers_test/req_with_headers_test.go b/seed/go-sdk/go-deterministic-ordering/reqwithheaders/req_with_headers_test/req_with_headers_test.go new file mode 100644 index 000000000000..91bba3f4c77e --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/reqwithheaders/req_with_headers_test/req_with_headers_test.go @@ -0,0 +1,90 @@ +// Code generated by Fern. DO NOT EDIT. + +package req_with_headers_test + +import ( + bytes "bytes" + context "context" + json "encoding/json" + fern "github.com/go-deterministic-ordering/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" + require "github.com/stretchr/testify/require" + http "net/http" + os "os" + testing "testing" +) + +func VerifyRequestCount( + t *testing.T, + testId string, + method string, + urlPath string, + queryParams map[string]string, + expected int, +) { + wiremockURL := os.Getenv("WIREMOCK_URL") + if wiremockURL == "" { + wiremockURL = "http://localhost:8080" + } + WiremockAdminURL := wiremockURL + "/__admin" + var reqBody bytes.Buffer + reqBody.WriteString(`{"method":"`) + reqBody.WriteString(method) + reqBody.WriteString(`","urlPath":"`) + reqBody.WriteString(urlPath) + reqBody.WriteString(`","headers":{"X-Test-Id":{"equalTo":"`) + reqBody.WriteString(testId) + reqBody.WriteString(`"}}`) + if len(queryParams) > 0 { + reqBody.WriteString(`,"queryParameters":{`) + first := true + for key, value := range queryParams { + if !first { + reqBody.WriteString(",") + } + reqBody.WriteString(`"`) + reqBody.WriteString(key) + reqBody.WriteString(`":{"equalTo":"`) + reqBody.WriteString(value) + reqBody.WriteString(`"}`) + first = false + } + reqBody.WriteString("}") + } + reqBody.WriteString("}") + resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody) + require.NoError(t, err) + var result struct { + Requests []interface{} `json:"requests"` + } + json.NewDecoder(resp.Body).Decode(&result) + require.Equal(t, expected, len(result.Requests)) +} + +func TestReqWithHeadersGetWithCustomHeaderWithWireMock( + t *testing.T, +) { + WireMockBaseURL := os.Getenv("WIREMOCK_URL") + if WireMockBaseURL == "" { + WireMockBaseURL = "http://localhost:8080" + } + client := client.NewClient( + option.WithBaseURL(WireMockBaseURL), + ) + request := &fern.ReqWithHeaders{ + XTestServiceHeader: "X-TEST-SERVICE-HEADER", + XTestEndpointHeader: "X-TEST-ENDPOINT-HEADER", + Body: "string", + } + invocationErr := client.ReqWithHeaders.GetWithCustomHeader( + context.TODO(), + request, + option.WithHTTPHeader( + http.Header{"X-Test-Id": []string{"TestReqWithHeadersGetWithCustomHeaderWithWireMock"}}, + ), + ) + + require.NoError(t, invocationErr, "Client method call should succeed") + VerifyRequestCount(t, "TestReqWithHeadersGetWithCustomHeaderWithWireMock", "POST", "/test-headers/custom-header", nil, 1) +} diff --git a/seed/go-sdk/go-deterministic-ordering/snippet.json b/seed/go-sdk/go-deterministic-ordering/snippet.json new file mode 100644 index 000000000000..10f768a2611e --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/snippet.json @@ -0,0 +1,697 @@ +{ + "endpoints": [ + { + "id": { + "path": "/container/list-of-objects", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnListOfObjects" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnListOfObjects(\n\tcontext.TODO(),\n\t[]*types.ObjectWithRequiredField{\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/container/list-of-primitives", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnListOfPrimitives" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnListOfPrimitives(\n\tcontext.TODO(),\n\t[]string{\n\t\t\"string\",\n\t\t\"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/container/map-prim-to-object", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnMapOfPrimToObject" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnMapOfPrimToObject(\n\tcontext.TODO(),\n\tmap[string]*types.ObjectWithRequiredField{\n\t\t\"string\": \u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/container/map-prim-to-prim", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnMapPrimToPrim" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnMapPrimToPrim(\n\tcontext.TODO(),\n\tmap[string]string{\n\t\t\"string\": \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/container/map-prim-to-union", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnMapOfPrimToUndiscriminatedUnion" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnMapOfPrimToUndiscriminatedUnion(\n\tcontext.TODO(),\n\tmap[string]*types.MixedType{\n\t\t\"string\": \u0026types.MixedType{\n\t\t\tDouble: 1.1,\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/container/opt-objects", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnOptional" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnOptional(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/container/set-of-objects", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnSetOfObjects" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnSetOfObjects(\n\tcontext.TODO(),\n\t[]*types.ObjectWithRequiredField{\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/container/set-of-primitives", + "method": "POST", + "identifier_override": "endpoint_endpoints/container.getAndReturnSetOfPrimitives" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnSetOfPrimitives(\n\tcontext.TODO(),\n\t[]string{\n\t\t\"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-a", + "method": "GET", + "identifier_override": "endpoint_endpoints/duplicate-names-a.list" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.DuplicateNamesA.List(\n\tcontext.TODO(),\n\t\u0026endpoints.ListRequestA{\n\t\tPage: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLimit: fern.Int(\n\t\t\t1,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-a", + "method": "POST", + "identifier_override": "endpoint_endpoints/duplicate-names-a.create" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.DuplicateNamesA.Create(\n\tcontext.TODO(),\n\t\u0026endpoints.CreateRequestA{\n\t\tName: \"name\",\n\t\tValue: 1,\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-a/{id}", + "method": "GET", + "identifier_override": "endpoint_endpoints/duplicate-names-a.get" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.DuplicateNamesA.Get(\n\tcontext.TODO(),\n\t\u0026endpoints.GetRequestA{\n\t\tId: \"id\",\n\t\tFilter: fern.String(\n\t\t\t\"filter\",\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-b", + "method": "GET", + "identifier_override": "endpoint_endpoints/duplicate-names-b.list" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.DuplicateNamesB.List(\n\tcontext.TODO(),\n\t\u0026endpoints.ListRequestB{\n\t\tCursor: fern.String(\n\t\t\t\"cursor\",\n\t\t),\n\t\tSize: fern.Int(\n\t\t\t1,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-b", + "method": "POST", + "identifier_override": "endpoint_endpoints/duplicate-names-b.create" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.DuplicateNamesB.Create(\n\tcontext.TODO(),\n\t\u0026endpoints.CreateRequestB{\n\t\tDescription: \"description\",\n\t\tCount: 1,\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-b/{id}", + "method": "GET", + "identifier_override": "endpoint_endpoints/duplicate-names-b.get" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.DuplicateNamesB.Get(\n\tcontext.TODO(),\n\t\u0026endpoints.GetRequestB{\n\t\tId: \"id\",\n\t\tExpand: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-c", + "method": "GET", + "identifier_override": "endpoint_endpoints/duplicate-names-c.list" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.DuplicateNamesC.List(\n\tcontext.TODO(),\n\t\u0026endpoints.ListRequestC{\n\t\tOffset: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tCount: fern.Int(\n\t\t\t1,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-c", + "method": "POST", + "identifier_override": "endpoint_endpoints/duplicate-names-c.create" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.DuplicateNamesC.Create(\n\tcontext.TODO(),\n\t\u0026endpoints.CreateRequestC{\n\t\tLabel: \"label\",\n\t\tPriority: 1,\n\t},\n)\n" + } + }, + { + "id": { + "path": "/duplicate-names-c/{id}", + "method": "GET", + "identifier_override": "endpoint_endpoints/duplicate-names-c.get" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.DuplicateNamesC.Get(\n\tcontext.TODO(),\n\t\u0026endpoints.GetRequestC{\n\t\tId: \"id\",\n\t\tVerbose: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/enum", + "method": "POST", + "identifier_override": "endpoint_endpoints/enum.getAndReturnEnum" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Enum.GetAndReturnEnum(\n\tcontext.TODO(),\n\ttypes.WeatherReportSunny,\n)\n" + } + }, + { + "id": { + "path": "/foo/bar", + "method": "POST", + "identifier_override": "endpoint_endpoints/content-type.postJsonPatchContentType" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.ContentType.PostJsonPatchContentType(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/foo/baz", + "method": "POST", + "identifier_override": "endpoint_endpoints/content-type.postJsonPatchContentWithCharsetType" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.ContentType.PostJsonPatchContentWithCharsetType(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/http-methods", + "method": "POST", + "identifier_override": "endpoint_endpoints/http-methods.testPost" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPost(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/http-methods/{id}", + "method": "DELETE", + "identifier_override": "endpoint_endpoints/http-methods.testDelete" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestDelete(\n\tcontext.TODO(),\n\t\"id\",\n)\n" + } + }, + { + "id": { + "path": "/http-methods/{id}", + "method": "GET", + "identifier_override": "endpoint_endpoints/http-methods.testGet" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestGet(\n\tcontext.TODO(),\n\t\"id\",\n)\n" + } + }, + { + "id": { + "path": "/http-methods/{id}", + "method": "PATCH", + "identifier_override": "endpoint_endpoints/http-methods.testPatch" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPatch(\n\tcontext.TODO(),\n\t\"id\",\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/http-methods/{id}", + "method": "PUT", + "identifier_override": "endpoint_endpoints/http-methods.testPut" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPut(\n\tcontext.TODO(),\n\t\"id\",\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/no-auth", + "method": "POST", + "identifier_override": "endpoint_no-auth.postWithNoAuth" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.NoAuth.PostWithNoAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/no-req-body", + "method": "GET", + "identifier_override": "endpoint_no-req-body.getWithNoRequestBody" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.NoReqBody.GetWithNoRequestBody(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/no-req-body", + "method": "POST", + "identifier_override": "endpoint_no-req-body.postWithNoRequestBody" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.NoReqBody.PostWithNoRequestBody(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-nested-with-optional-field", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnNestedWithOptionalField" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithOptionalField(\n\tcontext.TODO(),\n\t\u0026types.NestedObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tFieldString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-nested-with-required-field-list", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnNestedWithRequiredFieldAsList" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithRequiredFieldAsList(\n\tcontext.TODO(),\n\t[]*types.NestedObjectWithRequiredField{\n\t\t\u0026types.NestedObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\t\tFieldString: fern.String(\n\t\t\t\t\t\"string\",\n\t\t\t\t),\n\t\t\t\tInteger: fern.Int(\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t\tLong: fern.Int64(\n\t\t\t\t\t1000000,\n\t\t\t\t),\n\t\t\t\tDouble: fern.Float64(\n\t\t\t\t\t1.1,\n\t\t\t\t),\n\t\t\t\tBool: fern.Bool(\n\t\t\t\t\ttrue,\n\t\t\t\t),\n\t\t\t\tDatetime: fern.Time(\n\t\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tDate: fern.Time(\n\t\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tUuid: fern.UUID(\n\t\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\t\tList: []string{\n\t\t\t\t\t\"list\",\n\t\t\t\t\t\"list\",\n\t\t\t\t},\n\t\t\t\tSet: []string{\n\t\t\t\t\t\"set\",\n\t\t\t\t},\n\t\t\t\tMap: map[int]string{\n\t\t\t\t\t1: \"map\",\n\t\t\t\t},\n\t\t\t\tBigint: fern.String(\n\t\t\t\t\t\"1000000\",\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t\u0026types.NestedObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\t\tFieldString: fern.String(\n\t\t\t\t\t\"string\",\n\t\t\t\t),\n\t\t\t\tInteger: fern.Int(\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t\tLong: fern.Int64(\n\t\t\t\t\t1000000,\n\t\t\t\t),\n\t\t\t\tDouble: fern.Float64(\n\t\t\t\t\t1.1,\n\t\t\t\t),\n\t\t\t\tBool: fern.Bool(\n\t\t\t\t\ttrue,\n\t\t\t\t),\n\t\t\t\tDatetime: fern.Time(\n\t\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tDate: fern.Time(\n\t\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tUuid: fern.UUID(\n\t\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\t\tList: []string{\n\t\t\t\t\t\"list\",\n\t\t\t\t\t\"list\",\n\t\t\t\t},\n\t\t\t\tSet: []string{\n\t\t\t\t\t\"set\",\n\t\t\t\t},\n\t\t\t\tMap: map[int]string{\n\t\t\t\t\t1: \"map\",\n\t\t\t\t},\n\t\t\t\tBigint: fern.String(\n\t\t\t\t\t\"1000000\",\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-nested-with-required-field/{string}", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnNestedWithRequiredField" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithRequiredField(\n\tcontext.TODO(),\n\t\"string\",\n\t\u0026types.NestedObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tFieldString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-with-datetime-like-string", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnWithDatetimeLikeString" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithDatetimeLikeString(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithDatetimeLikeString{\n\t\tDatetimeLikeString: \"2023-08-31T14:15:22Z\",\n\t\tActualDatetime: fern.MustParseDateTime(\n\t\t\t\"2023-08-31T14:15:22Z\",\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-with-map-of-map", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnWithMapOfMap" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithMapOfMap(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithMapOfMap{\n\t\tMap: map[string]map[string]string{\n\t\t\t\"map\": map[string]string{\n\t\t\t\t\"map\": \"map\",\n\t\t\t},\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-with-optional-field", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnWithOptionalField" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithOptionalField(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-with-required-field", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnWithRequiredField" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithRequiredField(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/object/get-and-return-with-unknown-field", + "method": "POST", + "identifier_override": "endpoint_endpoints/object.getAndReturnWithUnknownField" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithUnknownField(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithUnknownField{\n\t\tUnknown: map[string]interface{}{\n\t\t\t\"$ref\": \"https://example.com/schema\",\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/pagination", + "method": "GET", + "identifier_override": "endpoint_endpoints/pagination.listItems" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Pagination.ListItems(\n\tcontext.TODO(),\n\t\u0026endpoints.ListItemsRequest{\n\t\tCursor: fern.String(\n\t\t\t\"cursor\",\n\t\t),\n\t\tLimit: fern.Int(\n\t\t\t1,\n\t\t),\n\t},\n)\n" + } + }, + { + "id": { + "path": "/params", + "method": "GET", + "identifier_override": "endpoint_endpoints/params.getWithAllowMultipleQuery" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.Params.GetWithAllowMultipleQuery(\n\tcontext.TODO(),\n\t\u0026endpoints.GetWithMultipleQuery{\n\t\tQuery: []string{\n\t\t\t\"query\",\n\t\t},\n\t\tNumber: []int{\n\t\t\t1,\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/params", + "method": "GET", + "identifier_override": "endpoint_endpoints/params.getWithQuery" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.Params.GetWithQuery(\n\tcontext.TODO(),\n\t\u0026endpoints.GetWithQuery{\n\t\tQuery: \"query\",\n\t\tNumber: 1,\n\t},\n)\n" + } + }, + { + "id": { + "path": "/params/path-query/{param}", + "method": "GET", + "identifier_override": "endpoint_endpoints/params.getWithInlinePathAndQuery" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.Params.GetWithInlinePathAndQuery(\n\tcontext.TODO(),\n\t\u0026endpoints.GetWithInlinePathAndQuery{\n\t\tParam: \"param\",\n\t\tQuery: \"query\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/params/path-query/{param}", + "method": "GET", + "identifier_override": "endpoint_endpoints/params.getWithPathAndQuery" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.Params.GetWithPathAndQuery(\n\tcontext.TODO(),\n\t\"param\",\n\t\u0026endpoints.GetWithPathAndQuery{\n\t\tQuery: \"query\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/params/path/{param}", + "method": "GET", + "identifier_override": "endpoint_endpoints/params.getWithInlinePath" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Params.GetWithInlinePath(\n\tcontext.TODO(),\n\t\u0026endpoints.GetWithInlinePath{\n\t\tParam: \"param\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/params/path/{param}", + "method": "GET", + "identifier_override": "endpoint_endpoints/params.getWithPath" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Params.GetWithPath(\n\tcontext.TODO(),\n\t\"param\",\n)\n" + } + }, + { + "id": { + "path": "/params/path/{param}", + "method": "POST", + "identifier_override": "endpoint_endpoints/params.uploadWithPath" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Params.UploadWithPath(\n\tcontext.TODO(),\n\t\"upload-path\",\n)\n" + } + }, + { + "id": { + "path": "/params/path/{param}", + "method": "PUT", + "identifier_override": "endpoint_endpoints/params.modifyWithInlinePath" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Params.ModifyWithInlinePath(\n\tcontext.TODO(),\n\t\u0026endpoints.ModifyResourceAtInlinedPath{\n\t\tParam: \"param\",\n\t\tBody: \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/params/path/{param}", + "method": "PUT", + "identifier_override": "endpoint_endpoints/params.modifyWithPath" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Params.ModifyWithPath(\n\tcontext.TODO(),\n\t\"param\",\n\t\"string\",\n)\n" + } + }, + { + "id": { + "path": "/primitive/base64", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnBase64" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnBase64(\n\tcontext.TODO(),\n\t[]byte(\"Hello world!\"),\n)\n" + } + }, + { + "id": { + "path": "/primitive/boolean", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnBool" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnBool(\n\tcontext.TODO(),\n\ttrue,\n)\n" + } + }, + { + "id": { + "path": "/primitive/date", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnDate" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnDate(\n\tcontext.TODO(),\n\tfern.MustParseDate(\n\t\t\"2023-01-15\",\n\t),\n)\n" + } + }, + { + "id": { + "path": "/primitive/datetime", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnDatetime" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnDatetime(\n\tcontext.TODO(),\n\tfern.MustParseDateTime(\n\t\t\"2024-01-15T09:30:00Z\",\n\t),\n)\n" + } + }, + { + "id": { + "path": "/primitive/double", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnDouble" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnDouble(\n\tcontext.TODO(),\n\t1.1,\n)\n" + } + }, + { + "id": { + "path": "/primitive/integer", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnInt" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnInt(\n\tcontext.TODO(),\n\t1,\n)\n" + } + }, + { + "id": { + "path": "/primitive/long", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnLong" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnLong(\n\tcontext.TODO(),\n\t1000000,\n)\n" + } + }, + { + "id": { + "path": "/primitive/string", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnString" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnString(\n\tcontext.TODO(),\n\t\"string\",\n)\n" + } + }, + { + "id": { + "path": "/primitive/uuid", + "method": "POST", + "identifier_override": "endpoint_endpoints/primitive.getAndReturnUUID" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Primitive.GetAndReturnUuid(\n\tcontext.TODO(),\n\tuuid.MustParse(\n\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t),\n)\n" + } + }, + { + "id": { + "path": "/req-bodies/object", + "method": "POST", + "identifier_override": "endpoint_inlined-requests.postWithObjectBodyandResponse" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.InlinedRequests.PostWithObjectBodyandResponse(\n\tcontext.TODO(),\n\t\u0026fern.PostWithObjectBody{\n\t\tFieldString: \"string\",\n\t\tInteger: 1,\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tFieldString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/test-headers/custom-header", + "method": "POST", + "identifier_override": "endpoint_req-with-headers.getWithCustomHeader" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/go-deterministic-ordering/fern\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.ReqWithHeaders.GetWithCustomHeader(\n\tcontext.TODO(),\n\t\u0026fern.ReqWithHeaders{\n\t\tXTestServiceHeader: \"X-TEST-SERVICE-HEADER\",\n\t\tXTestEndpointHeader: \"X-TEST-ENDPOINT-HEADER\",\n\t\tBody: \"string\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/union", + "method": "POST", + "identifier_override": "endpoint_endpoints/union.getAndReturnUnion" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n\ttypes \"github.com/go-deterministic-ordering/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Union.GetAndReturnUnion(\n\tcontext.TODO(),\n\t\u0026types.Animal{\n\t\tDog: \u0026types.Dog{\n\t\t\tName: \"name\",\n\t\t\tLikesToWoof: true,\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/urls/MixedCase", + "method": "GET", + "identifier_override": "endpoint_endpoints/urls.withMixedCase" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Urls.WithMixedCase(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/urls/no-ending-slash", + "method": "GET", + "identifier_override": "endpoint_endpoints/urls.noEndingSlash" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Urls.NoEndingSlash(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/urls/with-ending-slash", + "method": "GET", + "identifier_override": "endpoint_endpoints/urls.withEndingSlash" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Urls.WithEndingSlash(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/urls/with_underscores", + "method": "GET", + "identifier_override": "endpoint_endpoints/urls.withUnderscores" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Urls.WithUnderscores(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/{id}", + "method": "PUT", + "identifier_override": "endpoint_endpoints/put.add" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tendpoints \"github.com/go-deterministic-ordering/fern/endpoints\"\n\tfernclient \"github.com/go-deterministic-ordering/fern/client\"\n\toption \"github.com/go-deterministic-ordering/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Put.Add(\n\tcontext.TODO(),\n\t\u0026endpoints.PutRequest{\n\t\tId: \"id\",\n\t},\n)\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/go-sdk/go-deterministic-ordering/types/docs.go b/seed/go-sdk/go-deterministic-ordering/types/docs.go new file mode 100644 index 000000000000..081a871fe6be --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/docs.go @@ -0,0 +1,158 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + big "math/big" +) + +var ( + objectWithDocsFieldFieldString = big.NewInt(1 << 0) +) + +type ObjectWithDocs struct { + // Characters that could lead to broken generated SDKs: + // + // Markdown Escapes: + // - \_: Escaped underscore (e.g., FOO\_BAR) + // - \*: Escaped asterisk + // + // JSDoc (JavaScript/TypeScript): + // - @: Used for JSDoc tags + // - {: }: Used for type definitions + // - <: >: HTML tags + // - *: Can interfere with comment blocks + // - /**: JSDoc comment start + // - ** /: JSDoc comment end + // - &: HTML entities + // + // XMLDoc (C#): + // - <: >: XML tags + // - &: ': ": <: >: XML special characters + // - {: }: Used for interpolated strings + // - ///: Comment marker + // - /**: Block comment start + // - ** /: Block comment end + // + // XMLDoc (C#) (Example of actual XML tags): + // See the docs for more info. + // Use getValue() to retrieve the value. + // Note: when count < 10 or count > 100, special handling applies. + // + // Javadoc (Java): + // - @: Used for Javadoc tags + // - <: >: HTML tags + // - &: HTML entities + // - *: Can interfere with comment blocks + // - /**: Javadoc comment start + // - ** /: Javadoc comment end + // + // Doxygen (C++): + // - \: Used for Doxygen commands + // - @: Alternative command prefix + // - <: >: XML/HTML tags + // - &: HTML entities + // - /**: C-style comment start + // - ** /: C-style comment end + // + // RDoc (Ruby): + // - :: Used in symbol notation + // - =: Section markers + // - #: Comment marker + // - =begin: Block comment start + // - =end: Block comment end + // - @: Instance variable prefix + // - $: Global variable prefix + // - %: String literal delimiter + // - #{: String interpolation start + // - }: String interpolation end + // + // PHPDoc (PHP): + // - @: Used for PHPDoc tags + // - {: }: Used for type definitions + // - $: Variable prefix + // - /**: PHPDoc comment start + // - ** /: PHPDoc comment end + // - *: Can interfere with comment blocks + // - &: HTML entities + FieldString string `json:"string" url:"string"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (o *ObjectWithDocs) GetFieldString() string { + if o == nil { + return "" + } + return o.FieldString +} + +func (o *ObjectWithDocs) GetExtraProperties() map[string]interface{} { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithDocs) require(field *big.Int) { + if o.explicitFields == nil { + o.explicitFields = big.NewInt(0) + } + o.explicitFields.Or(o.explicitFields, field) +} + +// SetFieldString sets the FieldString field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithDocs) SetFieldString(string_ string) { + o.FieldString = string_ + o.require(objectWithDocsFieldFieldString) +} + +func (o *ObjectWithDocs) UnmarshalJSON(data []byte) error { + type unmarshaler ObjectWithDocs + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithDocs(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithDocs) MarshalJSON() ([]byte, error) { + type embed ObjectWithDocs + var marshaler = struct { + embed + }{ + embed: embed(*o), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, o.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (o *ObjectWithDocs) String() string { + if o == nil { + return "" + } + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} diff --git a/seed/go-sdk/go-deterministic-ordering/types/docs_test.go b/seed/go-sdk/go-deterministic-ordering/types/docs_test.go new file mode 100644 index 000000000000..d68670315f0c --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/docs_test.go @@ -0,0 +1,153 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" +) + +func TestSettersObjectWithDocs(t *testing.T) { + t.Run("SetFieldString", func(t *testing.T) { + obj := &ObjectWithDocs{} + var fernTestValueFieldString string + obj.SetFieldString(fernTestValueFieldString) + assert.Equal(t, fernTestValueFieldString, obj.FieldString) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersObjectWithDocs(t *testing.T) { + t.Run("GetFieldString", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDocs{} + var expected string + obj.FieldString = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetFieldString(), "getter should return the property value") + }) + + t.Run("GetFieldString_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithDocs + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetFieldString() // Should return zero value + }) + +} + +func TestSettersMarkExplicitObjectWithDocs(t *testing.T) { + t.Run("SetFieldString_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDocs{} + var fernTestValueFieldString string + + // Act + obj.SetFieldString(fernTestValueFieldString) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestJSONMarshalingObjectWithDocs(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDocs{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled ObjectWithDocs + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj ObjectWithDocs + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj ObjectWithDocs + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestStringObjectWithDocs(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithDocs{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithDocs + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestExtraPropertiesObjectWithDocs(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithDocs{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithDocs + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/types/enum.go b/seed/go-sdk/go-deterministic-ordering/types/enum.go new file mode 100644 index 000000000000..76c7732cb892 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/enum.go @@ -0,0 +1,35 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + fmt "fmt" +) + +type WeatherReport string + +const ( + WeatherReportSunny WeatherReport = "SUNNY" + WeatherReportCloudy WeatherReport = "CLOUDY" + WeatherReportRaining WeatherReport = "RAINING" + WeatherReportSnowing WeatherReport = "SNOWING" +) + +func NewWeatherReportFromString(s string) (WeatherReport, error) { + switch s { + case "SUNNY": + return WeatherReportSunny, nil + case "CLOUDY": + return WeatherReportCloudy, nil + case "RAINING": + return WeatherReportRaining, nil + case "SNOWING": + return WeatherReportSnowing, nil + } + var t WeatherReport + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (w WeatherReport) Ptr() *WeatherReport { + return &w +} diff --git a/seed/go-sdk/go-deterministic-ordering/types/enum_test.go b/seed/go-sdk/go-deterministic-ordering/types/enum_test.go new file mode 100644 index 000000000000..440107379d65 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/enum_test.go @@ -0,0 +1,51 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + assert "github.com/stretchr/testify/assert" + testing "testing" +) + +func TestEnumWeatherReport(t *testing.T) { + t.Run("NewFromString_SUNNY", func(t *testing.T) { + t.Parallel() + val, err := NewWeatherReportFromString("SUNNY") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, WeatherReport("SUNNY"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_CLOUDY", func(t *testing.T) { + t.Parallel() + val, err := NewWeatherReportFromString("CLOUDY") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, WeatherReport("CLOUDY"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_RAINING", func(t *testing.T) { + t.Parallel() + val, err := NewWeatherReportFromString("RAINING") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, WeatherReport("RAINING"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_SNOWING", func(t *testing.T) { + t.Parallel() + val, err := NewWeatherReportFromString("SNOWING") + assert.NoError(t, err, "valid enum value should not return error") + assert.Equal(t, WeatherReport("SNOWING"), val, "enum value should match expected wire value") + }) + + t.Run("NewFromString_Invalid", func(t *testing.T) { + _, err := NewWeatherReportFromString("invalid_value_that_does_not_exist") + assert.Error(t, err) + }) + + t.Run("Ptr", func(t *testing.T) { + val, err := NewWeatherReportFromString("SUNNY") + assert.NoError(t, err) + ptr := val.Ptr() + assert.NotNil(t, ptr) + assert.Equal(t, val, *ptr) + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/types/errors.go b/seed/go-sdk/go-deterministic-ordering/types/errors.go new file mode 100644 index 000000000000..90e5c15d29fe --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/errors.go @@ -0,0 +1,146 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + core "github.com/go-deterministic-ordering/fern/core" +) + +type ErrorWithEnumBody struct { + *core.APIError + Body WeatherReport +} + +func (e *ErrorWithEnumBody) UnmarshalJSON(data []byte) error { + var body WeatherReport + if err := json.Unmarshal(data, &body); err != nil { + return err + } + e.StatusCode = 400 + e.Body = body + return nil +} + +func (e *ErrorWithEnumBody) MarshalJSON() ([]byte, error) { + return json.Marshal(e.Body) +} + +func (e *ErrorWithEnumBody) Unwrap() error { + return e.APIError +} + +type NestedObjectWithOptionalFieldError struct { + *core.APIError + Body *NestedObjectWithOptionalField +} + +func (n *NestedObjectWithOptionalFieldError) UnmarshalJSON(data []byte) error { + var body *NestedObjectWithOptionalField + if err := json.Unmarshal(data, &body); err != nil { + return err + } + n.StatusCode = 400 + n.Body = body + return nil +} + +func (n *NestedObjectWithOptionalFieldError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) +} + +func (n *NestedObjectWithOptionalFieldError) Unwrap() error { + return n.APIError +} + +type NestedObjectWithRequiredFieldError struct { + *core.APIError + Body *NestedObjectWithRequiredField +} + +func (n *NestedObjectWithRequiredFieldError) UnmarshalJSON(data []byte) error { + var body *NestedObjectWithRequiredField + if err := json.Unmarshal(data, &body); err != nil { + return err + } + n.StatusCode = 400 + n.Body = body + return nil +} + +func (n *NestedObjectWithRequiredFieldError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) +} + +func (n *NestedObjectWithRequiredFieldError) Unwrap() error { + return n.APIError +} + +type ObjectWithOptionalFieldError struct { + *core.APIError + Body *ObjectWithOptionalField +} + +func (o *ObjectWithOptionalFieldError) UnmarshalJSON(data []byte) error { + var body *ObjectWithOptionalField + if err := json.Unmarshal(data, &body); err != nil { + return err + } + o.StatusCode = 400 + o.Body = body + return nil +} + +func (o *ObjectWithOptionalFieldError) MarshalJSON() ([]byte, error) { + return json.Marshal(o.Body) +} + +func (o *ObjectWithOptionalFieldError) Unwrap() error { + return o.APIError +} + +type ObjectWithRequiredFieldError struct { + *core.APIError + Body *ObjectWithRequiredField +} + +func (o *ObjectWithRequiredFieldError) UnmarshalJSON(data []byte) error { + var body *ObjectWithRequiredField + if err := json.Unmarshal(data, &body); err != nil { + return err + } + o.StatusCode = 400 + o.Body = body + return nil +} + +func (o *ObjectWithRequiredFieldError) MarshalJSON() ([]byte, error) { + return json.Marshal(o.Body) +} + +func (o *ObjectWithRequiredFieldError) Unwrap() error { + return o.APIError +} + +type ErrorWithUnionBody struct { + *core.APIError + Body *Animal +} + +func (e *ErrorWithUnionBody) UnmarshalJSON(data []byte) error { + var body *Animal + if err := json.Unmarshal(data, &body); err != nil { + return err + } + e.StatusCode = 400 + e.Body = body + return nil +} + +func (e *ErrorWithUnionBody) MarshalJSON() ([]byte, error) { + return json.Marshal(e.Body) +} + +func (e *ErrorWithUnionBody) Unwrap() error { + return e.APIError +} diff --git a/seed/go-sdk/go-deterministic-ordering/types/object.go b/seed/go-sdk/go-deterministic-ordering/types/object.go new file mode 100644 index 000000000000..5b1d89599601 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/object.go @@ -0,0 +1,954 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + uuid "github.com/google/uuid" + big "math/big" + time "time" +) + +var ( + doubleOptionalFieldOptionalAlias = big.NewInt(1 << 0) +) + +type DoubleOptional struct { + OptionalAlias *OptionalAlias `json:"optionalAlias,omitempty" url:"optionalAlias,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (d *DoubleOptional) GetOptionalAlias() *OptionalAlias { + if d == nil { + return nil + } + return d.OptionalAlias +} + +func (d *DoubleOptional) GetExtraProperties() map[string]interface{} { + if d == nil { + return nil + } + return d.extraProperties +} + +func (d *DoubleOptional) require(field *big.Int) { + if d.explicitFields == nil { + d.explicitFields = big.NewInt(0) + } + d.explicitFields.Or(d.explicitFields, field) +} + +// SetOptionalAlias sets the OptionalAlias field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (d *DoubleOptional) SetOptionalAlias(optionalAlias *OptionalAlias) { + d.OptionalAlias = optionalAlias + d.require(doubleOptionalFieldOptionalAlias) +} + +func (d *DoubleOptional) UnmarshalJSON(data []byte) error { + type unmarshaler DoubleOptional + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *d = DoubleOptional(value) + extraProperties, err := internal.ExtractExtraProperties(data, *d) + if err != nil { + return err + } + d.extraProperties = extraProperties + d.rawJSON = json.RawMessage(data) + return nil +} + +func (d *DoubleOptional) MarshalJSON() ([]byte, error) { + type embed DoubleOptional + var marshaler = struct { + embed + }{ + embed: embed(*d), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, d.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (d *DoubleOptional) String() string { + if d == nil { + return "" + } + if len(d.rawJSON) > 0 { + if value, err := internal.StringifyJSON(d.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(d); err == nil { + return value + } + return fmt.Sprintf("%#v", d) +} + +var ( + nestedObjectWithOptionalFieldFieldFieldString = big.NewInt(1 << 0) + nestedObjectWithOptionalFieldFieldNestedObject = big.NewInt(1 << 1) +) + +type NestedObjectWithOptionalField struct { + FieldString *string `json:"string,omitempty" url:"string,omitempty"` + NestedObject *ObjectWithOptionalField `json:"NestedObject,omitempty" url:"NestedObject,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (n *NestedObjectWithOptionalField) GetFieldString() *string { + if n == nil { + return nil + } + return n.FieldString +} + +func (n *NestedObjectWithOptionalField) GetNestedObject() *ObjectWithOptionalField { + if n == nil { + return nil + } + return n.NestedObject +} + +func (n *NestedObjectWithOptionalField) GetExtraProperties() map[string]interface{} { + if n == nil { + return nil + } + return n.extraProperties +} + +func (n *NestedObjectWithOptionalField) require(field *big.Int) { + if n.explicitFields == nil { + n.explicitFields = big.NewInt(0) + } + n.explicitFields.Or(n.explicitFields, field) +} + +// SetFieldString sets the FieldString field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (n *NestedObjectWithOptionalField) SetFieldString(string_ *string) { + n.FieldString = string_ + n.require(nestedObjectWithOptionalFieldFieldFieldString) +} + +// SetNestedObject sets the NestedObject field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (n *NestedObjectWithOptionalField) SetNestedObject(nestedObject *ObjectWithOptionalField) { + n.NestedObject = nestedObject + n.require(nestedObjectWithOptionalFieldFieldNestedObject) +} + +func (n *NestedObjectWithOptionalField) UnmarshalJSON(data []byte) error { + type unmarshaler NestedObjectWithOptionalField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *n = NestedObjectWithOptionalField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *n) + if err != nil { + return err + } + n.extraProperties = extraProperties + n.rawJSON = json.RawMessage(data) + return nil +} + +func (n *NestedObjectWithOptionalField) MarshalJSON() ([]byte, error) { + type embed NestedObjectWithOptionalField + var marshaler = struct { + embed + }{ + embed: embed(*n), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, n.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (n *NestedObjectWithOptionalField) String() string { + if n == nil { + return "" + } + if len(n.rawJSON) > 0 { + if value, err := internal.StringifyJSON(n.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(n); err == nil { + return value + } + return fmt.Sprintf("%#v", n) +} + +var ( + nestedObjectWithRequiredFieldFieldFieldString = big.NewInt(1 << 0) + nestedObjectWithRequiredFieldFieldNestedObject = big.NewInt(1 << 1) +) + +type NestedObjectWithRequiredField struct { + FieldString string `json:"string" url:"string"` + NestedObject *ObjectWithOptionalField `json:"NestedObject" url:"NestedObject"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (n *NestedObjectWithRequiredField) GetFieldString() string { + if n == nil { + return "" + } + return n.FieldString +} + +func (n *NestedObjectWithRequiredField) GetNestedObject() *ObjectWithOptionalField { + if n == nil { + return nil + } + return n.NestedObject +} + +func (n *NestedObjectWithRequiredField) GetExtraProperties() map[string]interface{} { + if n == nil { + return nil + } + return n.extraProperties +} + +func (n *NestedObjectWithRequiredField) require(field *big.Int) { + if n.explicitFields == nil { + n.explicitFields = big.NewInt(0) + } + n.explicitFields.Or(n.explicitFields, field) +} + +// SetFieldString sets the FieldString field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (n *NestedObjectWithRequiredField) SetFieldString(string_ string) { + n.FieldString = string_ + n.require(nestedObjectWithRequiredFieldFieldFieldString) +} + +// SetNestedObject sets the NestedObject field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (n *NestedObjectWithRequiredField) SetNestedObject(nestedObject *ObjectWithOptionalField) { + n.NestedObject = nestedObject + n.require(nestedObjectWithRequiredFieldFieldNestedObject) +} + +func (n *NestedObjectWithRequiredField) UnmarshalJSON(data []byte) error { + type unmarshaler NestedObjectWithRequiredField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *n = NestedObjectWithRequiredField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *n) + if err != nil { + return err + } + n.extraProperties = extraProperties + n.rawJSON = json.RawMessage(data) + return nil +} + +func (n *NestedObjectWithRequiredField) MarshalJSON() ([]byte, error) { + type embed NestedObjectWithRequiredField + var marshaler = struct { + embed + }{ + embed: embed(*n), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, n.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (n *NestedObjectWithRequiredField) String() string { + if n == nil { + return "" + } + if len(n.rawJSON) > 0 { + if value, err := internal.StringifyJSON(n.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(n); err == nil { + return value + } + return fmt.Sprintf("%#v", n) +} + +// This type tests that string fields containing datetime-like values +// are NOT reformatted by the wire test generator. The string field +// should preserve its exact value even if it looks like a datetime. +var ( + objectWithDatetimeLikeStringFieldDatetimeLikeString = big.NewInt(1 << 0) + objectWithDatetimeLikeStringFieldActualDatetime = big.NewInt(1 << 1) +) + +type ObjectWithDatetimeLikeString struct { + // A string field that happens to contain a datetime-like value + DatetimeLikeString string `json:"datetimeLikeString" url:"datetimeLikeString"` + // An actual datetime field for comparison + ActualDatetime time.Time `json:"actualDatetime" url:"actualDatetime"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (o *ObjectWithDatetimeLikeString) GetDatetimeLikeString() string { + if o == nil { + return "" + } + return o.DatetimeLikeString +} + +func (o *ObjectWithDatetimeLikeString) GetActualDatetime() time.Time { + if o == nil { + return time.Time{} + } + return o.ActualDatetime +} + +func (o *ObjectWithDatetimeLikeString) GetExtraProperties() map[string]interface{} { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithDatetimeLikeString) require(field *big.Int) { + if o.explicitFields == nil { + o.explicitFields = big.NewInt(0) + } + o.explicitFields.Or(o.explicitFields, field) +} + +// SetDatetimeLikeString sets the DatetimeLikeString field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithDatetimeLikeString) SetDatetimeLikeString(datetimeLikeString string) { + o.DatetimeLikeString = datetimeLikeString + o.require(objectWithDatetimeLikeStringFieldDatetimeLikeString) +} + +// SetActualDatetime sets the ActualDatetime field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithDatetimeLikeString) SetActualDatetime(actualDatetime time.Time) { + o.ActualDatetime = actualDatetime + o.require(objectWithDatetimeLikeStringFieldActualDatetime) +} + +func (o *ObjectWithDatetimeLikeString) UnmarshalJSON(data []byte) error { + type embed ObjectWithDatetimeLikeString + var unmarshaler = struct { + embed + ActualDatetime *internal.DateTime `json:"actualDatetime"` + }{ + embed: embed(*o), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *o = ObjectWithDatetimeLikeString(unmarshaler.embed) + o.ActualDatetime = unmarshaler.ActualDatetime.Time() + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithDatetimeLikeString) MarshalJSON() ([]byte, error) { + type embed ObjectWithDatetimeLikeString + var marshaler = struct { + embed + ActualDatetime *internal.DateTime `json:"actualDatetime"` + }{ + embed: embed(*o), + ActualDatetime: internal.NewDateTime(o.ActualDatetime), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, o.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (o *ObjectWithDatetimeLikeString) String() string { + if o == nil { + return "" + } + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +var ( + objectWithMapOfMapFieldMap = big.NewInt(1 << 0) +) + +type ObjectWithMapOfMap struct { + Map map[string]map[string]string `json:"map" url:"map"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (o *ObjectWithMapOfMap) GetMap() map[string]map[string]string { + if o == nil { + return nil + } + return o.Map +} + +func (o *ObjectWithMapOfMap) GetExtraProperties() map[string]interface{} { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithMapOfMap) require(field *big.Int) { + if o.explicitFields == nil { + o.explicitFields = big.NewInt(0) + } + o.explicitFields.Or(o.explicitFields, field) +} + +// SetMap sets the Map field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithMapOfMap) SetMap(map_ map[string]map[string]string) { + o.Map = map_ + o.require(objectWithMapOfMapFieldMap) +} + +func (o *ObjectWithMapOfMap) UnmarshalJSON(data []byte) error { + type unmarshaler ObjectWithMapOfMap + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithMapOfMap(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithMapOfMap) MarshalJSON() ([]byte, error) { + type embed ObjectWithMapOfMap + var marshaler = struct { + embed + }{ + embed: embed(*o), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, o.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (o *ObjectWithMapOfMap) String() string { + if o == nil { + return "" + } + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +var ( + objectWithOptionalFieldFieldFieldString = big.NewInt(1 << 0) + objectWithOptionalFieldFieldInteger = big.NewInt(1 << 1) + objectWithOptionalFieldFieldLong = big.NewInt(1 << 2) + objectWithOptionalFieldFieldDouble = big.NewInt(1 << 3) + objectWithOptionalFieldFieldBool = big.NewInt(1 << 4) + objectWithOptionalFieldFieldDatetime = big.NewInt(1 << 5) + objectWithOptionalFieldFieldDate = big.NewInt(1 << 6) + objectWithOptionalFieldFieldUuid = big.NewInt(1 << 7) + objectWithOptionalFieldFieldBase64 = big.NewInt(1 << 8) + objectWithOptionalFieldFieldList = big.NewInt(1 << 9) + objectWithOptionalFieldFieldSet = big.NewInt(1 << 10) + objectWithOptionalFieldFieldMap = big.NewInt(1 << 11) + objectWithOptionalFieldFieldBigint = big.NewInt(1 << 12) +) + +type ObjectWithOptionalField struct { + // This is a rather long descriptor of this single field in a more complex type. If you ask me I think this is a pretty good description for this field all things considered. + FieldString *string `json:"string,omitempty" url:"string,omitempty"` + Integer *int `json:"integer,omitempty" url:"integer,omitempty"` + Long *int64 `json:"long,omitempty" url:"long,omitempty"` + Double *float64 `json:"double,omitempty" url:"double,omitempty"` + Bool *bool `json:"bool,omitempty" url:"bool,omitempty"` + Datetime *time.Time `json:"datetime,omitempty" url:"datetime,omitempty"` + Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"` + Uuid *uuid.UUID `json:"uuid,omitempty" url:"uuid,omitempty"` + Base64 *[]byte `json:"base64,omitempty" url:"base64,omitempty"` + List []string `json:"list,omitempty" url:"list,omitempty"` + Set []string `json:"set,omitempty" url:"set,omitempty"` + Map map[int]string `json:"map,omitempty" url:"map,omitempty"` + Bigint *string `json:"bigint,omitempty" url:"bigint,omitempty"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (o *ObjectWithOptionalField) GetFieldString() *string { + if o == nil { + return nil + } + return o.FieldString +} + +func (o *ObjectWithOptionalField) GetInteger() *int { + if o == nil { + return nil + } + return o.Integer +} + +func (o *ObjectWithOptionalField) GetLong() *int64 { + if o == nil { + return nil + } + return o.Long +} + +func (o *ObjectWithOptionalField) GetDouble() *float64 { + if o == nil { + return nil + } + return o.Double +} + +func (o *ObjectWithOptionalField) GetBool() *bool { + if o == nil { + return nil + } + return o.Bool +} + +func (o *ObjectWithOptionalField) GetDatetime() *time.Time { + if o == nil { + return nil + } + return o.Datetime +} + +func (o *ObjectWithOptionalField) GetDate() *time.Time { + if o == nil { + return nil + } + return o.Date +} + +func (o *ObjectWithOptionalField) GetUuid() *uuid.UUID { + if o == nil { + return nil + } + return o.Uuid +} + +func (o *ObjectWithOptionalField) GetBase64() *[]byte { + if o == nil { + return nil + } + return o.Base64 +} + +func (o *ObjectWithOptionalField) GetList() []string { + if o == nil { + return nil + } + return o.List +} + +func (o *ObjectWithOptionalField) GetSet() []string { + if o == nil { + return nil + } + return o.Set +} + +func (o *ObjectWithOptionalField) GetMap() map[int]string { + if o == nil { + return nil + } + return o.Map +} + +func (o *ObjectWithOptionalField) GetBigint() *string { + if o == nil { + return nil + } + return o.Bigint +} + +func (o *ObjectWithOptionalField) GetExtraProperties() map[string]interface{} { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithOptionalField) require(field *big.Int) { + if o.explicitFields == nil { + o.explicitFields = big.NewInt(0) + } + o.explicitFields.Or(o.explicitFields, field) +} + +// SetFieldString sets the FieldString field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetFieldString(string_ *string) { + o.FieldString = string_ + o.require(objectWithOptionalFieldFieldFieldString) +} + +// SetInteger sets the Integer field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetInteger(integer *int) { + o.Integer = integer + o.require(objectWithOptionalFieldFieldInteger) +} + +// SetLong sets the Long field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetLong(long *int64) { + o.Long = long + o.require(objectWithOptionalFieldFieldLong) +} + +// SetDouble sets the Double field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetDouble(double *float64) { + o.Double = double + o.require(objectWithOptionalFieldFieldDouble) +} + +// SetBool sets the Bool field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetBool(bool_ *bool) { + o.Bool = bool_ + o.require(objectWithOptionalFieldFieldBool) +} + +// SetDatetime sets the Datetime field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetDatetime(datetime *time.Time) { + o.Datetime = datetime + o.require(objectWithOptionalFieldFieldDatetime) +} + +// SetDate sets the Date field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetDate(date *time.Time) { + o.Date = date + o.require(objectWithOptionalFieldFieldDate) +} + +// SetUuid sets the Uuid field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetUuid(uuid *uuid.UUID) { + o.Uuid = uuid + o.require(objectWithOptionalFieldFieldUuid) +} + +// SetBase64 sets the Base64 field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetBase64(base64 *[]byte) { + o.Base64 = base64 + o.require(objectWithOptionalFieldFieldBase64) +} + +// SetList sets the List field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetList(list []string) { + o.List = list + o.require(objectWithOptionalFieldFieldList) +} + +// SetSet sets the Set field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetSet(set []string) { + o.Set = set + o.require(objectWithOptionalFieldFieldSet) +} + +// SetMap sets the Map field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetMap(map_ map[int]string) { + o.Map = map_ + o.require(objectWithOptionalFieldFieldMap) +} + +// SetBigint sets the Bigint field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithOptionalField) SetBigint(bigint *string) { + o.Bigint = bigint + o.require(objectWithOptionalFieldFieldBigint) +} + +func (o *ObjectWithOptionalField) UnmarshalJSON(data []byte) error { + type embed ObjectWithOptionalField + var unmarshaler = struct { + embed + Datetime *internal.DateTime `json:"datetime,omitempty"` + Date *internal.Date `json:"date,omitempty"` + }{ + embed: embed(*o), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *o = ObjectWithOptionalField(unmarshaler.embed) + o.Datetime = unmarshaler.Datetime.TimePtr() + o.Date = unmarshaler.Date.TimePtr() + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithOptionalField) MarshalJSON() ([]byte, error) { + type embed ObjectWithOptionalField + var marshaler = struct { + embed + Datetime *internal.DateTime `json:"datetime,omitempty"` + Date *internal.Date `json:"date,omitempty"` + }{ + embed: embed(*o), + Datetime: internal.NewOptionalDateTime(o.Datetime), + Date: internal.NewOptionalDate(o.Date), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, o.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (o *ObjectWithOptionalField) String() string { + if o == nil { + return "" + } + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +var ( + objectWithRequiredFieldFieldFieldString = big.NewInt(1 << 0) +) + +type ObjectWithRequiredField struct { + FieldString string `json:"string" url:"string"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (o *ObjectWithRequiredField) GetFieldString() string { + if o == nil { + return "" + } + return o.FieldString +} + +func (o *ObjectWithRequiredField) GetExtraProperties() map[string]interface{} { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithRequiredField) require(field *big.Int) { + if o.explicitFields == nil { + o.explicitFields = big.NewInt(0) + } + o.explicitFields.Or(o.explicitFields, field) +} + +// SetFieldString sets the FieldString field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithRequiredField) SetFieldString(string_ string) { + o.FieldString = string_ + o.require(objectWithRequiredFieldFieldFieldString) +} + +func (o *ObjectWithRequiredField) UnmarshalJSON(data []byte) error { + type unmarshaler ObjectWithRequiredField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithRequiredField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithRequiredField) MarshalJSON() ([]byte, error) { + type embed ObjectWithRequiredField + var marshaler = struct { + embed + }{ + embed: embed(*o), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, o.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (o *ObjectWithRequiredField) String() string { + if o == nil { + return "" + } + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +// Tests that unknown/any values containing backslashes in map keys +// are properly escaped in Go string literals. +var ( + objectWithUnknownFieldFieldUnknown = big.NewInt(1 << 0) +) + +type ObjectWithUnknownField struct { + Unknown interface{} `json:"unknown" url:"unknown"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (o *ObjectWithUnknownField) GetUnknown() interface{} { + if o == nil { + return nil + } + return o.Unknown +} + +func (o *ObjectWithUnknownField) GetExtraProperties() map[string]interface{} { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithUnknownField) require(field *big.Int) { + if o.explicitFields == nil { + o.explicitFields = big.NewInt(0) + } + o.explicitFields.Or(o.explicitFields, field) +} + +// SetUnknown sets the Unknown field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (o *ObjectWithUnknownField) SetUnknown(unknown interface{}) { + o.Unknown = unknown + o.require(objectWithUnknownFieldFieldUnknown) +} + +func (o *ObjectWithUnknownField) UnmarshalJSON(data []byte) error { + type unmarshaler ObjectWithUnknownField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithUnknownField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithUnknownField) MarshalJSON() ([]byte, error) { + type embed ObjectWithUnknownField + var marshaler = struct { + embed + }{ + embed: embed(*o), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, o.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (o *ObjectWithUnknownField) String() string { + if o == nil { + return "" + } + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +type OptionalAlias = *string diff --git a/seed/go-sdk/go-deterministic-ordering/types/object_test.go b/seed/go-sdk/go-deterministic-ordering/types/object_test.go new file mode 100644 index 000000000000..c8a3a44db266 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/object_test.go @@ -0,0 +1,2193 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" + time "time" +) + +func TestSettersDoubleOptional(t *testing.T) { + t.Run("SetOptionalAlias", func(t *testing.T) { + obj := &DoubleOptional{} + var fernTestValueOptionalAlias *OptionalAlias + obj.SetOptionalAlias(fernTestValueOptionalAlias) + assert.Equal(t, fernTestValueOptionalAlias, obj.OptionalAlias) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersDoubleOptional(t *testing.T) { + t.Run("GetOptionalAlias", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &DoubleOptional{} + var expected *OptionalAlias + obj.OptionalAlias = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetOptionalAlias(), "getter should return the property value") + }) + + t.Run("GetOptionalAlias_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &DoubleOptional{} + obj.OptionalAlias = nil + + // Act & Assert + assert.Nil(t, obj.GetOptionalAlias(), "getter should return nil when property is nil") + }) + + t.Run("GetOptionalAlias_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *DoubleOptional + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetOptionalAlias() // Should return zero value + }) + +} + +func TestSettersMarkExplicitDoubleOptional(t *testing.T) { + t.Run("SetOptionalAlias_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &DoubleOptional{} + var fernTestValueOptionalAlias *OptionalAlias + + // Act + obj.SetOptionalAlias(fernTestValueOptionalAlias) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersNestedObjectWithOptionalField(t *testing.T) { + t.Run("SetFieldString", func(t *testing.T) { + obj := &NestedObjectWithOptionalField{} + var fernTestValueFieldString *string + obj.SetFieldString(fernTestValueFieldString) + assert.Equal(t, fernTestValueFieldString, obj.FieldString) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetNestedObject", func(t *testing.T) { + obj := &NestedObjectWithOptionalField{} + var fernTestValueNestedObject *ObjectWithOptionalField + obj.SetNestedObject(fernTestValueNestedObject) + assert.Equal(t, fernTestValueNestedObject, obj.NestedObject) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersNestedObjectWithOptionalField(t *testing.T) { + t.Run("GetFieldString", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithOptionalField{} + var expected *string + obj.FieldString = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetFieldString(), "getter should return the property value") + }) + + t.Run("GetFieldString_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithOptionalField{} + obj.FieldString = nil + + // Act & Assert + assert.Nil(t, obj.GetFieldString(), "getter should return nil when property is nil") + }) + + t.Run("GetFieldString_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetFieldString() // Should return zero value + }) + + t.Run("GetNestedObject", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithOptionalField{} + var expected *ObjectWithOptionalField + obj.NestedObject = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetNestedObject(), "getter should return the property value") + }) + + t.Run("GetNestedObject_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithOptionalField{} + obj.NestedObject = nil + + // Act & Assert + assert.Nil(t, obj.GetNestedObject(), "getter should return nil when property is nil") + }) + + t.Run("GetNestedObject_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetNestedObject() // Should return zero value + }) + +} + +func TestSettersMarkExplicitNestedObjectWithOptionalField(t *testing.T) { + t.Run("SetFieldString_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithOptionalField{} + var fernTestValueFieldString *string + + // Act + obj.SetFieldString(fernTestValueFieldString) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetNestedObject_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithOptionalField{} + var fernTestValueNestedObject *ObjectWithOptionalField + + // Act + obj.SetNestedObject(fernTestValueNestedObject) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersNestedObjectWithRequiredField(t *testing.T) { + t.Run("SetFieldString", func(t *testing.T) { + obj := &NestedObjectWithRequiredField{} + var fernTestValueFieldString string + obj.SetFieldString(fernTestValueFieldString) + assert.Equal(t, fernTestValueFieldString, obj.FieldString) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetNestedObject", func(t *testing.T) { + obj := &NestedObjectWithRequiredField{} + var fernTestValueNestedObject *ObjectWithOptionalField + obj.SetNestedObject(fernTestValueNestedObject) + assert.Equal(t, fernTestValueNestedObject, obj.NestedObject) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersNestedObjectWithRequiredField(t *testing.T) { + t.Run("GetFieldString", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithRequiredField{} + var expected string + obj.FieldString = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetFieldString(), "getter should return the property value") + }) + + t.Run("GetFieldString_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithRequiredField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetFieldString() // Should return zero value + }) + + t.Run("GetNestedObject", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithRequiredField{} + var expected *ObjectWithOptionalField + obj.NestedObject = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetNestedObject(), "getter should return the property value") + }) + + t.Run("GetNestedObject_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithRequiredField{} + obj.NestedObject = nil + + // Act & Assert + assert.Nil(t, obj.GetNestedObject(), "getter should return nil when property is nil") + }) + + t.Run("GetNestedObject_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithRequiredField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetNestedObject() // Should return zero value + }) + +} + +func TestSettersMarkExplicitNestedObjectWithRequiredField(t *testing.T) { + t.Run("SetFieldString_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithRequiredField{} + var fernTestValueFieldString string + + // Act + obj.SetFieldString(fernTestValueFieldString) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetNestedObject_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithRequiredField{} + var fernTestValueNestedObject *ObjectWithOptionalField + + // Act + obj.SetNestedObject(fernTestValueNestedObject) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersObjectWithDatetimeLikeString(t *testing.T) { + t.Run("SetDatetimeLikeString", func(t *testing.T) { + obj := &ObjectWithDatetimeLikeString{} + var fernTestValueDatetimeLikeString string + obj.SetDatetimeLikeString(fernTestValueDatetimeLikeString) + assert.Equal(t, fernTestValueDatetimeLikeString, obj.DatetimeLikeString) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetActualDatetime", func(t *testing.T) { + obj := &ObjectWithDatetimeLikeString{} + var fernTestValueActualDatetime time.Time + obj.SetActualDatetime(fernTestValueActualDatetime) + assert.Equal(t, fernTestValueActualDatetime, obj.ActualDatetime) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersObjectWithDatetimeLikeString(t *testing.T) { + t.Run("GetDatetimeLikeString", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDatetimeLikeString{} + var expected string + obj.DatetimeLikeString = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetDatetimeLikeString(), "getter should return the property value") + }) + + t.Run("GetDatetimeLikeString_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithDatetimeLikeString + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetDatetimeLikeString() // Should return zero value + }) + + t.Run("GetActualDatetime", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDatetimeLikeString{} + var expected time.Time + obj.ActualDatetime = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetActualDatetime(), "getter should return the property value") + }) + + t.Run("GetActualDatetime_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithDatetimeLikeString + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetActualDatetime() // Should return zero value + }) + +} + +func TestSettersMarkExplicitObjectWithDatetimeLikeString(t *testing.T) { + t.Run("SetDatetimeLikeString_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDatetimeLikeString{} + var fernTestValueDatetimeLikeString string + + // Act + obj.SetDatetimeLikeString(fernTestValueDatetimeLikeString) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetActualDatetime_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDatetimeLikeString{} + var fernTestValueActualDatetime time.Time + + // Act + obj.SetActualDatetime(fernTestValueActualDatetime) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersObjectWithMapOfMap(t *testing.T) { + t.Run("SetMap", func(t *testing.T) { + obj := &ObjectWithMapOfMap{} + var fernTestValueMap map[string]map[string]string + obj.SetMap(fernTestValueMap) + assert.Equal(t, fernTestValueMap, obj.Map) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersObjectWithMapOfMap(t *testing.T) { + t.Run("GetMap", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithMapOfMap{} + var expected map[string]map[string]string + obj.Map = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetMap(), "getter should return the property value") + }) + + t.Run("GetMap_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithMapOfMap{} + obj.Map = nil + + // Act & Assert + assert.Nil(t, obj.GetMap(), "getter should return nil when property is nil") + }) + + t.Run("GetMap_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithMapOfMap + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetMap() // Should return zero value + }) + +} + +func TestSettersMarkExplicitObjectWithMapOfMap(t *testing.T) { + t.Run("SetMap_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithMapOfMap{} + var fernTestValueMap map[string]map[string]string + + // Act + obj.SetMap(fernTestValueMap) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersObjectWithOptionalField(t *testing.T) { + t.Run("SetFieldString", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueFieldString *string + obj.SetFieldString(fernTestValueFieldString) + assert.Equal(t, fernTestValueFieldString, obj.FieldString) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetInteger", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueInteger *int + obj.SetInteger(fernTestValueInteger) + assert.Equal(t, fernTestValueInteger, obj.Integer) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetLong", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueLong *int64 + obj.SetLong(fernTestValueLong) + assert.Equal(t, fernTestValueLong, obj.Long) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetDouble", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueDouble *float64 + obj.SetDouble(fernTestValueDouble) + assert.Equal(t, fernTestValueDouble, obj.Double) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetBool", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueBool *bool + obj.SetBool(fernTestValueBool) + assert.Equal(t, fernTestValueBool, obj.Bool) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetDatetime", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueDatetime *time.Time + obj.SetDatetime(fernTestValueDatetime) + assert.Equal(t, fernTestValueDatetime, obj.Datetime) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetDate", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueDate *time.Time + obj.SetDate(fernTestValueDate) + assert.Equal(t, fernTestValueDate, obj.Date) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetBase64", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueBase64 *[]byte + obj.SetBase64(fernTestValueBase64) + assert.Equal(t, fernTestValueBase64, obj.Base64) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetList", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueList []string + obj.SetList(fernTestValueList) + assert.Equal(t, fernTestValueList, obj.List) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetSet", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueSet []string + obj.SetSet(fernTestValueSet) + assert.Equal(t, fernTestValueSet, obj.Set) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetMap", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueMap map[int]string + obj.SetMap(fernTestValueMap) + assert.Equal(t, fernTestValueMap, obj.Map) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetBigint", func(t *testing.T) { + obj := &ObjectWithOptionalField{} + var fernTestValueBigint *string + obj.SetBigint(fernTestValueBigint) + assert.Equal(t, fernTestValueBigint, obj.Bigint) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersObjectWithOptionalField(t *testing.T) { + t.Run("GetFieldString", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *string + obj.FieldString = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetFieldString(), "getter should return the property value") + }) + + t.Run("GetFieldString_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.FieldString = nil + + // Act & Assert + assert.Nil(t, obj.GetFieldString(), "getter should return nil when property is nil") + }) + + t.Run("GetFieldString_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetFieldString() // Should return zero value + }) + + t.Run("GetInteger", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *int + obj.Integer = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetInteger(), "getter should return the property value") + }) + + t.Run("GetInteger_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Integer = nil + + // Act & Assert + assert.Nil(t, obj.GetInteger(), "getter should return nil when property is nil") + }) + + t.Run("GetInteger_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetInteger() // Should return zero value + }) + + t.Run("GetLong", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *int64 + obj.Long = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetLong(), "getter should return the property value") + }) + + t.Run("GetLong_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Long = nil + + // Act & Assert + assert.Nil(t, obj.GetLong(), "getter should return nil when property is nil") + }) + + t.Run("GetLong_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetLong() // Should return zero value + }) + + t.Run("GetDouble", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *float64 + obj.Double = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetDouble(), "getter should return the property value") + }) + + t.Run("GetDouble_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Double = nil + + // Act & Assert + assert.Nil(t, obj.GetDouble(), "getter should return nil when property is nil") + }) + + t.Run("GetDouble_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetDouble() // Should return zero value + }) + + t.Run("GetBool", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *bool + obj.Bool = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetBool(), "getter should return the property value") + }) + + t.Run("GetBool_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Bool = nil + + // Act & Assert + assert.Nil(t, obj.GetBool(), "getter should return nil when property is nil") + }) + + t.Run("GetBool_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetBool() // Should return zero value + }) + + t.Run("GetDatetime", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *time.Time + obj.Datetime = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetDatetime(), "getter should return the property value") + }) + + t.Run("GetDatetime_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Datetime = nil + + // Act & Assert + assert.Nil(t, obj.GetDatetime(), "getter should return nil when property is nil") + }) + + t.Run("GetDatetime_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetDatetime() // Should return zero value + }) + + t.Run("GetDate", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *time.Time + obj.Date = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetDate(), "getter should return the property value") + }) + + t.Run("GetDate_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Date = nil + + // Act & Assert + assert.Nil(t, obj.GetDate(), "getter should return nil when property is nil") + }) + + t.Run("GetDate_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetDate() // Should return zero value + }) + + t.Run("GetBase64", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *[]byte + obj.Base64 = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetBase64(), "getter should return the property value") + }) + + t.Run("GetBase64_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Base64 = nil + + // Act & Assert + assert.Nil(t, obj.GetBase64(), "getter should return nil when property is nil") + }) + + t.Run("GetBase64_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetBase64() // Should return zero value + }) + + t.Run("GetList", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected []string + obj.List = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetList(), "getter should return the property value") + }) + + t.Run("GetList_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.List = nil + + // Act & Assert + assert.Nil(t, obj.GetList(), "getter should return nil when property is nil") + }) + + t.Run("GetList_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetList() // Should return zero value + }) + + t.Run("GetSet", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected []string + obj.Set = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetSet(), "getter should return the property value") + }) + + t.Run("GetSet_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Set = nil + + // Act & Assert + assert.Nil(t, obj.GetSet(), "getter should return nil when property is nil") + }) + + t.Run("GetSet_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetSet() // Should return zero value + }) + + t.Run("GetMap", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected map[int]string + obj.Map = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetMap(), "getter should return the property value") + }) + + t.Run("GetMap_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Map = nil + + // Act & Assert + assert.Nil(t, obj.GetMap(), "getter should return nil when property is nil") + }) + + t.Run("GetMap_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetMap() // Should return zero value + }) + + t.Run("GetBigint", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var expected *string + obj.Bigint = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetBigint(), "getter should return the property value") + }) + + t.Run("GetBigint_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + obj.Bigint = nil + + // Act & Assert + assert.Nil(t, obj.GetBigint(), "getter should return nil when property is nil") + }) + + t.Run("GetBigint_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetBigint() // Should return zero value + }) + +} + +func TestSettersMarkExplicitObjectWithOptionalField(t *testing.T) { + t.Run("SetFieldString_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueFieldString *string + + // Act + obj.SetFieldString(fernTestValueFieldString) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetInteger_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueInteger *int + + // Act + obj.SetInteger(fernTestValueInteger) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetLong_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueLong *int64 + + // Act + obj.SetLong(fernTestValueLong) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetDouble_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueDouble *float64 + + // Act + obj.SetDouble(fernTestValueDouble) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetBool_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueBool *bool + + // Act + obj.SetBool(fernTestValueBool) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetDatetime_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueDatetime *time.Time + + // Act + obj.SetDatetime(fernTestValueDatetime) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetDate_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueDate *time.Time + + // Act + obj.SetDate(fernTestValueDate) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetBase64_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueBase64 *[]byte + + // Act + obj.SetBase64(fernTestValueBase64) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetList_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueList []string + + // Act + obj.SetList(fernTestValueList) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetSet_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueSet []string + + // Act + obj.SetSet(fernTestValueSet) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetMap_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueMap map[int]string + + // Act + obj.SetMap(fernTestValueMap) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetBigint_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + var fernTestValueBigint *string + + // Act + obj.SetBigint(fernTestValueBigint) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersObjectWithRequiredField(t *testing.T) { + t.Run("SetFieldString", func(t *testing.T) { + obj := &ObjectWithRequiredField{} + var fernTestValueFieldString string + obj.SetFieldString(fernTestValueFieldString) + assert.Equal(t, fernTestValueFieldString, obj.FieldString) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersObjectWithRequiredField(t *testing.T) { + t.Run("GetFieldString", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithRequiredField{} + var expected string + obj.FieldString = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetFieldString(), "getter should return the property value") + }) + + t.Run("GetFieldString_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithRequiredField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetFieldString() // Should return zero value + }) + +} + +func TestSettersMarkExplicitObjectWithRequiredField(t *testing.T) { + t.Run("SetFieldString_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithRequiredField{} + var fernTestValueFieldString string + + // Act + obj.SetFieldString(fernTestValueFieldString) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersObjectWithUnknownField(t *testing.T) { + t.Run("SetUnknown", func(t *testing.T) { + obj := &ObjectWithUnknownField{} + var fernTestValueUnknown interface{} + obj.SetUnknown(fernTestValueUnknown) + assert.Equal(t, fernTestValueUnknown, obj.Unknown) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersObjectWithUnknownField(t *testing.T) { + t.Run("GetUnknown", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithUnknownField{} + var expected interface{} + obj.Unknown = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetUnknown(), "getter should return the property value") + }) + + t.Run("GetUnknown_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithUnknownField + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetUnknown() // Should return zero value + }) + +} + +func TestSettersMarkExplicitObjectWithUnknownField(t *testing.T) { + t.Run("SetUnknown_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithUnknownField{} + var fernTestValueUnknown interface{} + + // Act + obj.SetUnknown(fernTestValueUnknown) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestJSONMarshalingDoubleOptional(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &DoubleOptional{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled DoubleOptional + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj DoubleOptional + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj DoubleOptional + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingNestedObjectWithOptionalField(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithOptionalField{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled NestedObjectWithOptionalField + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj NestedObjectWithOptionalField + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj NestedObjectWithOptionalField + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingNestedObjectWithRequiredField(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &NestedObjectWithRequiredField{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled NestedObjectWithRequiredField + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj NestedObjectWithRequiredField + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj NestedObjectWithRequiredField + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingObjectWithDatetimeLikeString(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithDatetimeLikeString{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled ObjectWithDatetimeLikeString + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj ObjectWithDatetimeLikeString + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj ObjectWithDatetimeLikeString + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingObjectWithMapOfMap(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithMapOfMap{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled ObjectWithMapOfMap + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj ObjectWithMapOfMap + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj ObjectWithMapOfMap + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingObjectWithOptionalField(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithOptionalField{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled ObjectWithOptionalField + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj ObjectWithOptionalField + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj ObjectWithOptionalField + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingObjectWithRequiredField(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithRequiredField{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled ObjectWithRequiredField + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj ObjectWithRequiredField + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj ObjectWithRequiredField + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingObjectWithUnknownField(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &ObjectWithUnknownField{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled ObjectWithUnknownField + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj ObjectWithUnknownField + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj ObjectWithUnknownField + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestStringDoubleOptional(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &DoubleOptional{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *DoubleOptional + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringNestedObjectWithOptionalField(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &NestedObjectWithOptionalField{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithOptionalField + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringNestedObjectWithRequiredField(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &NestedObjectWithRequiredField{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithRequiredField + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringObjectWithDatetimeLikeString(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithDatetimeLikeString{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithDatetimeLikeString + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringObjectWithMapOfMap(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithMapOfMap{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithMapOfMap + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringObjectWithOptionalField(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithOptionalField{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringObjectWithRequiredField(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithRequiredField{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithRequiredField + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringObjectWithUnknownField(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithUnknownField{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithUnknownField + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestExtraPropertiesDoubleOptional(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &DoubleOptional{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *DoubleOptional + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesNestedObjectWithOptionalField(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &NestedObjectWithOptionalField{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithOptionalField + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesNestedObjectWithRequiredField(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &NestedObjectWithRequiredField{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *NestedObjectWithRequiredField + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesObjectWithDatetimeLikeString(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithDatetimeLikeString{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithDatetimeLikeString + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesObjectWithMapOfMap(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithMapOfMap{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithMapOfMap + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesObjectWithOptionalField(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithOptionalField{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithOptionalField + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesObjectWithRequiredField(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithRequiredField{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithRequiredField + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesObjectWithUnknownField(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &ObjectWithUnknownField{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *ObjectWithUnknownField + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/types/union.go b/seed/go-sdk/go-deterministic-ordering/types/union.go new file mode 100644 index 000000000000..89e0a500b405 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/union.go @@ -0,0 +1,431 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + big "math/big" +) + +type Animal struct { + Animal string + Dog *Dog + Cat *Cat +} + +func (a *Animal) GetAnimal() string { + if a == nil { + return "" + } + return a.Animal +} + +func (a *Animal) GetDog() *Dog { + if a == nil { + return nil + } + return a.Dog +} + +func (a *Animal) GetCat() *Cat { + if a == nil { + return nil + } + return a.Cat +} + +func (a *Animal) UnmarshalJSON(data []byte) error { + var unmarshaler struct { + Animal string `json:"animal"` + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + a.Animal = unmarshaler.Animal + if unmarshaler.Animal == "" { + return fmt.Errorf("%T did not include discriminant animal", a) + } + switch unmarshaler.Animal { + case "dog": + value := new(Dog) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + a.Dog = value + case "cat": + value := new(Cat) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + a.Cat = value + } + return nil +} + +func (a Animal) MarshalJSON() ([]byte, error) { + if err := a.validate(); err != nil { + return nil, err + } + if a.Dog != nil { + return internal.MarshalJSONWithExtraProperty(a.Dog, "animal", "dog") + } + if a.Cat != nil { + return internal.MarshalJSONWithExtraProperty(a.Cat, "animal", "cat") + } + return nil, fmt.Errorf("type %T does not define a non-empty union type", a) +} + +type AnimalVisitor interface { + VisitDog(*Dog) error + VisitCat(*Cat) error +} + +func (a *Animal) Accept(visitor AnimalVisitor) error { + if a.Dog != nil { + return visitor.VisitDog(a.Dog) + } + if a.Cat != nil { + return visitor.VisitCat(a.Cat) + } + return fmt.Errorf("type %T does not define a non-empty union type", a) +} + +func (a *Animal) validate() error { + if a == nil { + return fmt.Errorf("type %T is nil", a) + } + var fields []string + if a.Dog != nil { + fields = append(fields, "dog") + } + if a.Cat != nil { + fields = append(fields, "cat") + } + if len(fields) == 0 { + if a.Animal != "" { + return fmt.Errorf("type %T defines a discriminant set to %q but the field is not set", a, a.Animal) + } + return fmt.Errorf("type %T is empty", a) + } + if len(fields) > 1 { + return fmt.Errorf("type %T defines values for %s, but only one value is allowed", a, fields) + } + if a.Animal != "" { + field := fields[0] + if a.Animal != field { + return fmt.Errorf( + "type %T defines a discriminant set to %q, but it does not match the %T field; either remove or update the discriminant to match", + a, + a.Animal, + a, + ) + } + } + return nil +} + +var ( + catFieldName = big.NewInt(1 << 0) + catFieldLikesToMeow = big.NewInt(1 << 1) +) + +type Cat struct { + Name string `json:"name" url:"name"` + LikesToMeow bool `json:"likesToMeow" url:"likesToMeow"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (c *Cat) GetName() string { + if c == nil { + return "" + } + return c.Name +} + +func (c *Cat) GetLikesToMeow() bool { + if c == nil { + return false + } + return c.LikesToMeow +} + +func (c *Cat) GetExtraProperties() map[string]interface{} { + if c == nil { + return nil + } + return c.extraProperties +} + +func (c *Cat) require(field *big.Int) { + if c.explicitFields == nil { + c.explicitFields = big.NewInt(0) + } + c.explicitFields.Or(c.explicitFields, field) +} + +// SetName sets the Name field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *Cat) SetName(name string) { + c.Name = name + c.require(catFieldName) +} + +// SetLikesToMeow sets the LikesToMeow field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (c *Cat) SetLikesToMeow(likesToMeow bool) { + c.LikesToMeow = likesToMeow + c.require(catFieldLikesToMeow) +} + +func (c *Cat) UnmarshalJSON(data []byte) error { + type unmarshaler Cat + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *c = Cat(value) + extraProperties, err := internal.ExtractExtraProperties(data, *c) + if err != nil { + return err + } + c.extraProperties = extraProperties + c.rawJSON = json.RawMessage(data) + return nil +} + +func (c *Cat) MarshalJSON() ([]byte, error) { + type embed Cat + var marshaler = struct { + embed + }{ + embed: embed(*c), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, c.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (c *Cat) String() string { + if c == nil { + return "" + } + if len(c.rawJSON) > 0 { + if value, err := internal.StringifyJSON(c.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(c); err == nil { + return value + } + return fmt.Sprintf("%#v", c) +} + +var ( + dogFieldName = big.NewInt(1 << 0) + dogFieldLikesToWoof = big.NewInt(1 << 1) +) + +type Dog struct { + Name string `json:"name" url:"name"` + LikesToWoof bool `json:"likesToWoof" url:"likesToWoof"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (d *Dog) GetName() string { + if d == nil { + return "" + } + return d.Name +} + +func (d *Dog) GetLikesToWoof() bool { + if d == nil { + return false + } + return d.LikesToWoof +} + +func (d *Dog) GetExtraProperties() map[string]interface{} { + if d == nil { + return nil + } + return d.extraProperties +} + +func (d *Dog) require(field *big.Int) { + if d.explicitFields == nil { + d.explicitFields = big.NewInt(0) + } + d.explicitFields.Or(d.explicitFields, field) +} + +// SetName sets the Name field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (d *Dog) SetName(name string) { + d.Name = name + d.require(dogFieldName) +} + +// SetLikesToWoof sets the LikesToWoof field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (d *Dog) SetLikesToWoof(likesToWoof bool) { + d.LikesToWoof = likesToWoof + d.require(dogFieldLikesToWoof) +} + +func (d *Dog) UnmarshalJSON(data []byte) error { + type unmarshaler Dog + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *d = Dog(value) + extraProperties, err := internal.ExtractExtraProperties(data, *d) + if err != nil { + return err + } + d.extraProperties = extraProperties + d.rawJSON = json.RawMessage(data) + return nil +} + +func (d *Dog) MarshalJSON() ([]byte, error) { + type embed Dog + var marshaler = struct { + embed + }{ + embed: embed(*d), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, d.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (d *Dog) String() string { + if d == nil { + return "" + } + if len(d.rawJSON) > 0 { + if value, err := internal.StringifyJSON(d.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(d); err == nil { + return value + } + return fmt.Sprintf("%#v", d) +} + +type MixedType struct { + Double float64 + Boolean bool + String string + StringList []string + + typ string +} + +func (m *MixedType) GetDouble() float64 { + if m == nil { + return 0 + } + return m.Double +} + +func (m *MixedType) GetBoolean() bool { + if m == nil { + return false + } + return m.Boolean +} + +func (m *MixedType) GetString() string { + if m == nil { + return "" + } + return m.String +} + +func (m *MixedType) GetStringList() []string { + if m == nil { + return nil + } + return m.StringList +} + +func (m *MixedType) UnmarshalJSON(data []byte) error { + var valueDouble float64 + if err := json.Unmarshal(data, &valueDouble); err == nil { + m.typ = "Double" + m.Double = valueDouble + return nil + } + var valueBoolean bool + if err := json.Unmarshal(data, &valueBoolean); err == nil { + m.typ = "Boolean" + m.Boolean = valueBoolean + return nil + } + var valueString string + if err := json.Unmarshal(data, &valueString); err == nil { + m.typ = "String" + m.String = valueString + return nil + } + var valueStringList []string + if err := json.Unmarshal(data, &valueStringList); err == nil { + m.typ = "StringList" + m.StringList = valueStringList + return nil + } + return fmt.Errorf("%s cannot be deserialized as a %T", data, m) +} + +func (m MixedType) MarshalJSON() ([]byte, error) { + if m.typ == "Double" || m.Double != 0 { + return json.Marshal(m.Double) + } + if m.typ == "Boolean" || m.Boolean != false { + return json.Marshal(m.Boolean) + } + if m.typ == "String" || m.String != "" { + return json.Marshal(m.String) + } + if m.typ == "StringList" || m.StringList != nil { + return json.Marshal(m.StringList) + } + return nil, fmt.Errorf("type %T does not include a non-empty union type", m) +} + +type MixedTypeVisitor interface { + VisitDouble(float64) error + VisitBoolean(bool) error + VisitString(string) error + VisitStringList([]string) error +} + +func (m *MixedType) Accept(visitor MixedTypeVisitor) error { + if m.typ == "Double" || m.Double != 0 { + return visitor.VisitDouble(m.Double) + } + if m.typ == "Boolean" || m.Boolean != false { + return visitor.VisitBoolean(m.Boolean) + } + if m.typ == "String" || m.String != "" { + return visitor.VisitString(m.String) + } + if m.typ == "StringList" || m.StringList != nil { + return visitor.VisitStringList(m.StringList) + } + return fmt.Errorf("type %T does not include a non-empty union type", m) +} diff --git a/seed/go-sdk/go-deterministic-ordering/types/union_test.go b/seed/go-sdk/go-deterministic-ordering/types/union_test.go new file mode 100644 index 000000000000..84304d195de6 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/types/union_test.go @@ -0,0 +1,617 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" +) + +func TestGettersAnimal(t *testing.T) { + t.Run("GetAnimal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Animal{} + var expected string + obj.Animal = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetAnimal(), "getter should return the property value") + }) + + t.Run("GetAnimal_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Animal + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetAnimal() // Should return zero value + }) + + t.Run("GetDog", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Animal{} + var expected *Dog + obj.Dog = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetDog(), "getter should return the property value") + }) + + t.Run("GetDog_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Animal{} + obj.Dog = nil + + // Act & Assert + assert.Nil(t, obj.GetDog(), "getter should return nil when property is nil") + }) + + t.Run("GetDog_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Animal + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetDog() // Should return zero value + }) + + t.Run("GetCat", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Animal{} + var expected *Cat + obj.Cat = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetCat(), "getter should return the property value") + }) + + t.Run("GetCat_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Animal{} + obj.Cat = nil + + // Act & Assert + assert.Nil(t, obj.GetCat(), "getter should return nil when property is nil") + }) + + t.Run("GetCat_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Animal + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetCat() // Should return zero value + }) + +} + +func TestSettersCat(t *testing.T) { + t.Run("SetName", func(t *testing.T) { + obj := &Cat{} + var fernTestValueName string + obj.SetName(fernTestValueName) + assert.Equal(t, fernTestValueName, obj.Name) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetLikesToMeow", func(t *testing.T) { + obj := &Cat{} + var fernTestValueLikesToMeow bool + obj.SetLikesToMeow(fernTestValueLikesToMeow) + assert.Equal(t, fernTestValueLikesToMeow, obj.LikesToMeow) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersCat(t *testing.T) { + t.Run("GetName", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Cat{} + var expected string + obj.Name = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetName(), "getter should return the property value") + }) + + t.Run("GetName_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Cat + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetName() // Should return zero value + }) + + t.Run("GetLikesToMeow", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Cat{} + var expected bool + obj.LikesToMeow = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetLikesToMeow(), "getter should return the property value") + }) + + t.Run("GetLikesToMeow_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Cat + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetLikesToMeow() // Should return zero value + }) + +} + +func TestSettersMarkExplicitCat(t *testing.T) { + t.Run("SetName_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Cat{} + var fernTestValueName string + + // Act + obj.SetName(fernTestValueName) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetLikesToMeow_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Cat{} + var fernTestValueLikesToMeow bool + + // Act + obj.SetLikesToMeow(fernTestValueLikesToMeow) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestSettersDog(t *testing.T) { + t.Run("SetName", func(t *testing.T) { + obj := &Dog{} + var fernTestValueName string + obj.SetName(fernTestValueName) + assert.Equal(t, fernTestValueName, obj.Name) + assert.NotNil(t, obj.explicitFields) + }) + + t.Run("SetLikesToWoof", func(t *testing.T) { + obj := &Dog{} + var fernTestValueLikesToWoof bool + obj.SetLikesToWoof(fernTestValueLikesToWoof) + assert.Equal(t, fernTestValueLikesToWoof, obj.LikesToWoof) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersDog(t *testing.T) { + t.Run("GetName", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Dog{} + var expected string + obj.Name = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetName(), "getter should return the property value") + }) + + t.Run("GetName_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Dog + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetName() // Should return zero value + }) + + t.Run("GetLikesToWoof", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Dog{} + var expected bool + obj.LikesToWoof = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetLikesToWoof(), "getter should return the property value") + }) + + t.Run("GetLikesToWoof_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Dog + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetLikesToWoof() // Should return zero value + }) + +} + +func TestSettersMarkExplicitDog(t *testing.T) { + t.Run("SetName_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Dog{} + var fernTestValueName string + + // Act + obj.SetName(fernTestValueName) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + + t.Run("SetLikesToWoof_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Dog{} + var fernTestValueLikesToWoof bool + + // Act + obj.SetLikesToWoof(fernTestValueLikesToWoof) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestGettersMixedType(t *testing.T) { + t.Run("GetDouble", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &MixedType{} + var expected float64 + obj.Double = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetDouble(), "getter should return the property value") + }) + + t.Run("GetDouble_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *MixedType + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetDouble() // Should return zero value + }) + + t.Run("GetBoolean", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &MixedType{} + var expected bool + obj.Boolean = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetBoolean(), "getter should return the property value") + }) + + t.Run("GetBoolean_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *MixedType + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetBoolean() // Should return zero value + }) + + t.Run("GetString", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &MixedType{} + var expected string + obj.String = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetString(), "getter should return the property value") + }) + + t.Run("GetString_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *MixedType + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetString() // Should return zero value + }) + + t.Run("GetStringList", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &MixedType{} + var expected []string + obj.StringList = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetStringList(), "getter should return the property value") + }) + + t.Run("GetStringList_NilValue", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &MixedType{} + obj.StringList = nil + + // Act & Assert + assert.Nil(t, obj.GetStringList(), "getter should return nil when property is nil") + }) + + t.Run("GetStringList_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *MixedType + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetStringList() // Should return zero value + }) + +} + +func TestJSONMarshalingCat(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Cat{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled Cat + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj Cat + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj Cat + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestJSONMarshalingDog(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &Dog{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled Dog + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj Dog + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj Dog + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestStringCat(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &Cat{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Cat + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestStringDog(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &Dog{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Dog + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestExtraPropertiesCat(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &Cat{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Cat + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} + +func TestExtraPropertiesDog(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &Dog{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *Dog + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} diff --git a/seed/go-sdk/go-deterministic-ordering/wiremock/docker-compose.test.yml b/seed/go-sdk/go-deterministic-ordering/wiremock/docker-compose.test.yml new file mode 100644 index 000000000000..fabb06a62ccd --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/wiremock/docker-compose.test.yml @@ -0,0 +1,14 @@ +services: + wiremock: + image: wiremock/wiremock:3.9.1 + ports: + - "0:8080" + volumes: + - ./wiremock-mappings.json:/home/wiremock/mappings/wiremock-mappings.json + command: ["--global-response-templating", "--verbose"] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"] + interval: 2s + timeout: 5s + retries: 15 + start_period: 5s diff --git a/seed/go-sdk/go-deterministic-ordering/wiremock/wiremock-mappings.json b/seed/go-sdk/go-deterministic-ordering/wiremock/wiremock-mappings.json new file mode 100644 index 000000000000..61f975ed9cd2 --- /dev/null +++ b/seed/go-sdk/go-deterministic-ordering/wiremock/wiremock-mappings.json @@ -0,0 +1 @@ +{"mappings":[{"id":"864d58e6-0c08-4f38-b7e1-2638f8d7fd6d","name":"getAndReturnListOfPrimitives - default","request":{"urlPathTemplate":"/container/list-of-primitives","method":"POST"},"response":{"status":200,"body":"[\n \"string\",\n \"string\"\n]","headers":{"Content-Type":"application/json"}},"uuid":"864d58e6-0c08-4f38-b7e1-2638f8d7fd6d","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"ac1d4c4f-a8a7-4c27-ae64-7fc977cfa122","name":"getAndReturnListOfObjects - default","request":{"urlPathTemplate":"/container/list-of-objects","method":"POST"},"response":{"status":200,"body":"[\n {\n \"string\": \"string\"\n },\n {\n \"string\": \"string\"\n }\n]","headers":{"Content-Type":"application/json"}},"uuid":"ac1d4c4f-a8a7-4c27-ae64-7fc977cfa122","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"591d5c48-a536-452b-8a2e-ad7c23c38298","name":"getAndReturnSetOfPrimitives - default","request":{"urlPathTemplate":"/container/set-of-primitives","method":"POST"},"response":{"status":200,"body":"[\n \"string\"\n]","headers":{"Content-Type":"application/json"}},"uuid":"591d5c48-a536-452b-8a2e-ad7c23c38298","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"e1d5f52b-7a51-464f-ac8f-83c0345a3a35","name":"getAndReturnSetOfObjects - default","request":{"urlPathTemplate":"/container/set-of-objects","method":"POST"},"response":{"status":200,"body":"[\n {\n \"string\": \"string\"\n }\n]","headers":{"Content-Type":"application/json"}},"uuid":"e1d5f52b-7a51-464f-ac8f-83c0345a3a35","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"4b1d33b3-ca7d-462a-a2e3-23d531ae2922","name":"getAndReturnMapPrimToPrim - default","request":{"urlPathTemplate":"/container/map-prim-to-prim","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"4b1d33b3-ca7d-462a-a2e3-23d531ae2922","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"b01ac2b9-3470-48aa-badc-57d331bb5a49","name":"getAndReturnMapOfPrimToObject - default","request":{"urlPathTemplate":"/container/map-prim-to-object","method":"POST"},"response":{"status":200,"body":"{\n \"string\": {\n \"string\": \"string\"\n }\n}","headers":{"Content-Type":"application/json"}},"uuid":"b01ac2b9-3470-48aa-badc-57d331bb5a49","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"eaf9315d-55c4-4434-8004-a70c25af5656","name":"getAndReturnMapOfPrimToUndiscriminatedUnion - default","request":{"urlPathTemplate":"/container/map-prim-to-union","method":"POST"},"response":{"status":200,"body":"{\n \"string\": 1.1\n}","headers":{"Content-Type":"application/json"}},"uuid":"eaf9315d-55c4-4434-8004-a70c25af5656","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"e5271904-de0a-425f-940d-d6f6bde34755","name":"getAndReturnOptional - default","request":{"urlPathTemplate":"/container/opt-objects","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"e5271904-de0a-425f-940d-d6f6bde34755","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"d19e1fbe-79cc-465c-962e-e1866ca2361b","name":"postJsonPatchContentType - default","request":{"urlPathTemplate":"/foo/bar","method":"POST"},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"d19e1fbe-79cc-465c-962e-e1866ca2361b","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"4a107cf5-6284-48f8-9ddb-99d944ba989b","name":"postJsonPatchContentWithCharsetType - default","request":{"urlPathTemplate":"/foo/baz","method":"POST"},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"4a107cf5-6284-48f8-9ddb-99d944ba989b","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"3d0f2e90-2d03-42dc-a74e-40df7a70124e","name":"create - default","request":{"urlPathTemplate":"/duplicate-names-a","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"3d0f2e90-2d03-42dc-a74e-40df7a70124e","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"11fa3079-0358-4e89-9f0d-64bbf1bf8ccb","name":"get - default","request":{"urlPathTemplate":"/duplicate-names-a/{id}","method":"GET","pathParameters":{"id":{"equalTo":"id"}},"queryParameters":{"filter":{"equalTo":"filter"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"11fa3079-0358-4e89-9f0d-64bbf1bf8ccb","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"3da46246-7305-4b4e-a454-3481a95d0bd8","name":"list - default","request":{"urlPathTemplate":"/duplicate-names-a","method":"GET","queryParameters":{"page":{"equalTo":"1"},"limit":{"equalTo":"1"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"3da46246-7305-4b4e-a454-3481a95d0bd8","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"ac29dc07-6a67-4f68-b5d5-c2ab1b941e77","name":"create - default","request":{"urlPathTemplate":"/duplicate-names-b","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"ac29dc07-6a67-4f68-b5d5-c2ab1b941e77","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"1d344a7f-e2db-46f8-ae62-cccae2b5540a","name":"get - default","request":{"urlPathTemplate":"/duplicate-names-b/{id}","method":"GET","pathParameters":{"id":{"equalTo":"id"}},"queryParameters":{"expand":{"equalTo":"true"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"1d344a7f-e2db-46f8-ae62-cccae2b5540a","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"70f9c378-aa0b-4d1f-b2c5-55b1d55c92f4","name":"list - default","request":{"urlPathTemplate":"/duplicate-names-b","method":"GET","queryParameters":{"cursor":{"equalTo":"cursor"},"size":{"equalTo":"1"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"70f9c378-aa0b-4d1f-b2c5-55b1d55c92f4","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"83272e08-ab7a-4351-9ae1-4739fbb2a631","name":"create - default","request":{"urlPathTemplate":"/duplicate-names-c","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"83272e08-ab7a-4351-9ae1-4739fbb2a631","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"64e8d831-e97f-4b08-9e12-2458aecd1c7d","name":"get - default","request":{"urlPathTemplate":"/duplicate-names-c/{id}","method":"GET","pathParameters":{"id":{"equalTo":"id"}},"queryParameters":{"verbose":{"equalTo":"true"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"64e8d831-e97f-4b08-9e12-2458aecd1c7d","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"f33a9912-4b32-408d-b578-ba557f6e0399","name":"list - default","request":{"urlPathTemplate":"/duplicate-names-c","method":"GET","queryParameters":{"offset":{"equalTo":"1"},"count":{"equalTo":"1"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"f33a9912-4b32-408d-b578-ba557f6e0399","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"60fd3c8a-3983-41b9-8178-f42997388900","name":"getAndReturnEnum - default","request":{"urlPathTemplate":"/enum","method":"POST"},"response":{"status":200,"body":"\"SUNNY\"","headers":{"Content-Type":"application/json"}},"uuid":"60fd3c8a-3983-41b9-8178-f42997388900","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"3d95c052-db6e-4eff-95f2-895666a5af54","name":"testGet - default","request":{"urlPathTemplate":"/http-methods/{id}","method":"GET","pathParameters":{"id":{"equalTo":"id"}}},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"3d95c052-db6e-4eff-95f2-895666a5af54","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"2552873f-9f1d-4557-a6c7-6c6b7a55b566","name":"testPost - default","request":{"urlPathTemplate":"/http-methods","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"2552873f-9f1d-4557-a6c7-6c6b7a55b566","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"db42fbdf-5426-41b7-b7ea-28ed39a38e82","name":"testPut - default","request":{"urlPathTemplate":"/http-methods/{id}","method":"PUT","pathParameters":{"id":{"equalTo":"id"}}},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"db42fbdf-5426-41b7-b7ea-28ed39a38e82","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"c9def317-32a0-4bc0-b9b6-5efae8ca44a6","name":"testPatch - default","request":{"urlPathTemplate":"/http-methods/{id}","method":"PATCH","pathParameters":{"id":{"equalTo":"id"}}},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"c9def317-32a0-4bc0-b9b6-5efae8ca44a6","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"05333635-c9ce-4c1a-bb6d-95e8a0fb80dc","name":"testDelete - default","request":{"urlPathTemplate":"/http-methods/{id}","method":"DELETE","pathParameters":{"id":{"equalTo":"id"}}},"response":{"status":200,"body":"true","headers":{"Content-Type":"application/json"}},"uuid":"05333635-c9ce-4c1a-bb6d-95e8a0fb80dc","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"41cdef0e-040f-4d08-8426-87b19e60f7d7","name":"getAndReturnWithOptionalField - default","request":{"urlPathTemplate":"/object/get-and-return-with-optional-field","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"41cdef0e-040f-4d08-8426-87b19e60f7d7","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"f74df550-df8c-4503-b202-0b9b3165c1a7","name":"getAndReturnWithRequiredField - default","request":{"urlPathTemplate":"/object/get-and-return-with-required-field","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"f74df550-df8c-4503-b202-0b9b3165c1a7","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"0c904dbb-ce54-48a2-8364-13a4989be7f2","name":"getAndReturnWithMapOfMap - default","request":{"urlPathTemplate":"/object/get-and-return-with-map-of-map","method":"POST"},"response":{"status":200,"body":"{\n \"map\": {\n \"map\": {\n \"map\": \"map\"\n }\n }\n}","headers":{"Content-Type":"application/json"}},"uuid":"0c904dbb-ce54-48a2-8364-13a4989be7f2","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"67e969fc-ff81-4a67-9858-66a29ffc9b72","name":"getAndReturnNestedWithOptionalField - default","request":{"urlPathTemplate":"/object/get-and-return-nested-with-optional-field","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"NestedObject\": {\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n }\n}","headers":{"Content-Type":"application/json"}},"uuid":"67e969fc-ff81-4a67-9858-66a29ffc9b72","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"169827d0-4247-4236-8cef-34b94d2659de","name":"getAndReturnNestedWithRequiredField - default","request":{"urlPathTemplate":"/object/get-and-return-nested-with-required-field/{string}","method":"POST","pathParameters":{"string":{"equalTo":"string"}}},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"NestedObject\": {\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n }\n}","headers":{"Content-Type":"application/json"}},"uuid":"169827d0-4247-4236-8cef-34b94d2659de","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"6d9ed308-5724-4bbc-a4f7-28dc264d188f","name":"getAndReturnNestedWithRequiredFieldAsList - default","request":{"urlPathTemplate":"/object/get-and-return-nested-with-required-field-list","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"NestedObject\": {\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n }\n}","headers":{"Content-Type":"application/json"}},"uuid":"6d9ed308-5724-4bbc-a4f7-28dc264d188f","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"233729b0-81c8-4fb3-aa53-793afa48096a","name":"getAndReturnWithUnknownField - default","request":{"urlPathTemplate":"/object/get-and-return-with-unknown-field","method":"POST"},"response":{"status":200,"body":"{\n \"unknown\": {\n \"$ref\": \"https://example.com/schema\"\n }\n}","headers":{"Content-Type":"application/json"}},"uuid":"233729b0-81c8-4fb3-aa53-793afa48096a","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"e2cc3e92-6e37-4a50-9081-23952fdd73fe","name":"getAndReturnWithDatetimeLikeString - default","request":{"urlPathTemplate":"/object/get-and-return-with-datetime-like-string","method":"POST"},"response":{"status":200,"body":"{\n \"datetimeLikeString\": \"2023-08-31T14:15:22Z\",\n \"actualDatetime\": \"2023-08-31T14:15:22Z\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"e2cc3e92-6e37-4a50-9081-23952fdd73fe","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"f15b2079-2482-4ed2-951d-d2b5c4de9afd","name":"listItems - default","request":{"urlPathTemplate":"/pagination","method":"GET","queryParameters":{"cursor":{"equalTo":"cursor"},"limit":{"equalTo":"1"}}},"response":{"status":200,"body":"{\n \"items\": [\n {\n \"string\": \"string\"\n },\n {\n \"string\": \"string\"\n }\n ],\n \"next\": \"next\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"f15b2079-2482-4ed2-951d-d2b5c4de9afd","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"97806fdb-f31f-4f90-84b8-f9cc1713d53d","name":"getWithPath - default","request":{"urlPathTemplate":"/params/path/{param}","method":"GET","pathParameters":{"param":{"equalTo":"param"}}},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"97806fdb-f31f-4f90-84b8-f9cc1713d53d","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"711fc64f-4af9-4084-8c29-1e7a9e58be70","name":"getWithInlinePath - default","request":{"urlPathTemplate":"/params/path/{param}","method":"GET","pathParameters":{"param":{"equalTo":"param"}}},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"711fc64f-4af9-4084-8c29-1e7a9e58be70","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"8e5739b3-d75f-47d7-b6b8-a663d91a66b5","name":"getWithQuery - default","request":{"urlPathTemplate":"/params","method":"GET","queryParameters":{"query":{"equalTo":"query"},"number":{"equalTo":"1"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"8e5739b3-d75f-47d7-b6b8-a663d91a66b5","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"4125d349-732b-4ff7-948d-1eeb977ed13b","name":"getWithAllowMultipleQuery - default","request":{"urlPathTemplate":"/params","method":"GET","queryParameters":{"query":{"equalTo":"query"},"number":{"equalTo":"1"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"4125d349-732b-4ff7-948d-1eeb977ed13b","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"1933d96a-a1b9-4cf1-85ba-c1e8eff5bd56","name":"getWithPathAndQuery - default","request":{"urlPathTemplate":"/params/path-query/{param}","method":"GET","pathParameters":{"param":{"equalTo":"param"}},"queryParameters":{"query":{"equalTo":"query"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"1933d96a-a1b9-4cf1-85ba-c1e8eff5bd56","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"c4f5012a-fb3f-45ac-b695-beedc3353ad8","name":"getWithInlinePathAndQuery - default","request":{"urlPathTemplate":"/params/path-query/{param}","method":"GET","pathParameters":{"param":{"equalTo":"param"}},"queryParameters":{"query":{"equalTo":"query"}}},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"c4f5012a-fb3f-45ac-b695-beedc3353ad8","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"046bf7d6-751b-48e9-bfc6-ff74a17e88e1","name":"modifyWithPath - default","request":{"urlPathTemplate":"/params/path/{param}","method":"PUT","pathParameters":{"param":{"equalTo":"param"}}},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"046bf7d6-751b-48e9-bfc6-ff74a17e88e1","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"6bfc7195-b99c-4449-bf47-7c4f74f6f33b","name":"modifyWithInlinePath - default","request":{"urlPathTemplate":"/params/path/{param}","method":"PUT","pathParameters":{"param":{"equalTo":"param"}}},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"6bfc7195-b99c-4449-bf47-7c4f74f6f33b","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"0506ae7c-87b7-4cd2-9e50-31d60f81893f","name":"getAndReturnString - default","request":{"urlPathTemplate":"/primitive/string","method":"POST"},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"0506ae7c-87b7-4cd2-9e50-31d60f81893f","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"67f875d1-e19e-440d-8f0c-7e1d24ef2619","name":"getAndReturnInt - default","request":{"urlPathTemplate":"/primitive/integer","method":"POST"},"response":{"status":200,"body":"1","headers":{"Content-Type":"application/json"}},"uuid":"67f875d1-e19e-440d-8f0c-7e1d24ef2619","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"5fadbebd-86c0-41f9-8be5-864e39eb5924","name":"getAndReturnLong - default","request":{"urlPathTemplate":"/primitive/long","method":"POST"},"response":{"status":200,"body":"1000000","headers":{"Content-Type":"application/json"}},"uuid":"5fadbebd-86c0-41f9-8be5-864e39eb5924","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"e03872b6-86b1-490b-9195-86e5d3a014f2","name":"getAndReturnDouble - default","request":{"urlPathTemplate":"/primitive/double","method":"POST"},"response":{"status":200,"body":"1.1","headers":{"Content-Type":"application/json"}},"uuid":"e03872b6-86b1-490b-9195-86e5d3a014f2","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"442e632f-890a-4105-9448-f7015127e3b4","name":"getAndReturnBool - default","request":{"urlPathTemplate":"/primitive/boolean","method":"POST"},"response":{"status":200,"body":"true","headers":{"Content-Type":"application/json"}},"uuid":"442e632f-890a-4105-9448-f7015127e3b4","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"ad76fa81-1e63-43fd-9a99-1d5e4339d98c","name":"getAndReturnDatetime - default","request":{"urlPathTemplate":"/primitive/datetime","method":"POST"},"response":{"status":200,"body":"\"2024-01-15T09:30:00Z\"","headers":{"Content-Type":"application/json"}},"uuid":"ad76fa81-1e63-43fd-9a99-1d5e4339d98c","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"afb524b1-54ab-4674-8446-fdd574c368cc","name":"getAndReturnDate - default","request":{"urlPathTemplate":"/primitive/date","method":"POST"},"response":{"status":200,"body":"\"2023-01-15\"","headers":{"Content-Type":"application/json"}},"uuid":"afb524b1-54ab-4674-8446-fdd574c368cc","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"969c0f3a-218a-45b7-b17f-64b1a9307d43","name":"getAndReturnUUID - default","request":{"urlPathTemplate":"/primitive/uuid","method":"POST"},"response":{"status":200,"body":"\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\"","headers":{"Content-Type":"application/json"}},"uuid":"969c0f3a-218a-45b7-b17f-64b1a9307d43","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"04baa20d-d318-40e6-9784-c40158c16acd","name":"getAndReturnBase64 - default","request":{"urlPathTemplate":"/primitive/base64","method":"POST"},"response":{"status":200,"body":"\"SGVsbG8gd29ybGQh\"","headers":{"Content-Type":"application/json"}},"uuid":"04baa20d-d318-40e6-9784-c40158c16acd","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"fddd5aaf-ab0d-4ef6-949c-5d329f6d7eb2","name":"Put - default","request":{"urlPathTemplate":"/{id}","method":"PUT","pathParameters":{"id":{"equalTo":"id"}}},"response":{"status":200,"body":"{\n \"errors\": [\n {\n \"category\": \"API_ERROR\",\n \"code\": \"INTERNAL_SERVER_ERROR\",\n \"detail\": \"detail\",\n \"field\": \"field\"\n },\n {\n \"category\": \"API_ERROR\",\n \"code\": \"INTERNAL_SERVER_ERROR\",\n \"detail\": \"detail\",\n \"field\": \"field\"\n }\n ]\n}","headers":{"Content-Type":"application/json"}},"uuid":"fddd5aaf-ab0d-4ef6-949c-5d329f6d7eb2","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"d29330f9-49ff-49c4-9081-7c83dbad315b","name":"getAndReturnUnion - default","request":{"urlPathTemplate":"/union","method":"POST"},"response":{"status":200,"body":"{\n \"animal\": \"dog\",\n \"name\": \"name\",\n \"likesToWoof\": true\n}","headers":{"Content-Type":"application/json"}},"uuid":"d29330f9-49ff-49c4-9081-7c83dbad315b","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"343f71f3-36ce-4684-b762-d60a086b43a4","name":"withMixedCase - default","request":{"urlPathTemplate":"/urls/MixedCase","method":"GET"},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"343f71f3-36ce-4684-b762-d60a086b43a4","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"48f36314-b2b7-4910-9e1d-5b05f3346a60","name":"noEndingSlash - default","request":{"urlPathTemplate":"/urls/no-ending-slash","method":"GET"},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"48f36314-b2b7-4910-9e1d-5b05f3346a60","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"f7b95029-2f25-4f70-8b4c-0855712747d8","name":"withEndingSlash - default","request":{"urlPathTemplate":"/urls/with-ending-slash/","method":"GET"},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"f7b95029-2f25-4f70-8b4c-0855712747d8","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"2f9671a5-e6da-43a8-be27-d0be0191dcbf","name":"withUnderscores - default","request":{"urlPathTemplate":"/urls/with_underscores","method":"GET"},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"2f9671a5-e6da-43a8-be27-d0be0191dcbf","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"4a6d0aaa-05f2-47bb-9f40-6e3743b9d2bf","name":"postWithObjectBodyandResponse - default","request":{"urlPathTemplate":"/req-bodies/object","method":"POST"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"4a6d0aaa-05f2-47bb-9f40-6e3743b9d2bf","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"c6e7cc6c-b76f-4860-a9ad-52dc8f55b6e6","name":"postWithNoAuth - default","request":{"urlPathTemplate":"/no-auth","method":"POST"},"response":{"status":200,"body":"true","headers":{"Content-Type":"application/json"}},"uuid":"c6e7cc6c-b76f-4860-a9ad-52dc8f55b6e6","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"8098eeea-bc6b-4068-9601-566c2092f83f","name":"getWithNoRequestBody - default","request":{"urlPathTemplate":"/no-req-body","method":"GET"},"response":{"status":200,"body":"{\n \"string\": \"string\",\n \"integer\": 1,\n \"long\": 1000000,\n \"double\": 1.1,\n \"bool\": true,\n \"datetime\": \"2024-01-15T09:30:00Z\",\n \"date\": \"2023-01-15\",\n \"uuid\": \"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n \"base64\": \"SGVsbG8gd29ybGQh\",\n \"list\": [\n \"list\",\n \"list\"\n ],\n \"set\": [\n \"set\"\n ],\n \"map\": {\n \"1\": \"map\"\n },\n \"bigint\": \"1000000\"\n}","headers":{"Content-Type":"application/json"}},"uuid":"8098eeea-bc6b-4068-9601-566c2092f83f","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}},"postServeActions":[]},{"id":"7dd9f944-1b35-42e9-a5ed-e48214ac8e91","name":"postWithNoRequestBody - default","request":{"urlPathTemplate":"/no-req-body","method":"POST"},"response":{"status":200,"body":"\"string\"","headers":{"Content-Type":"application/json"}},"uuid":"7dd9f944-1b35-42e9-a5ed-e48214ac8e91","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}},{"id":"d7b54168-aef4-4d68-a9c5-446e97dee2fc","name":"getWithCustomHeader - default","request":{"urlPathTemplate":"/test-headers/custom-header","method":"POST"},"response":{"status":200,"body":"\"\"","headers":{"Content-Type":"application/json"}},"uuid":"d7b54168-aef4-4d68-a9c5-446e97dee2fc","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}}],"meta":{"total":62}} \ No newline at end of file diff --git a/seed/go-sdk/seed.yml b/seed/go-sdk/seed.yml index d99c7877e8c7..a72fce711c19 100644 --- a/seed/go-sdk/seed.yml +++ b/seed/go-sdk/seed.yml @@ -270,6 +270,11 @@ fixtures: packageName: fern module: path: github.com/go-undiscriminated-union-wire-tests/fern + go-deterministic-ordering: + - outputFolder: . + customConfig: + enableWireTests: true + exportAllRequestsAtRoot: true exhaustive: - outputFolder: no-custom-config customConfig: diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/api.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/api.yml new file mode 100644 index 000000000000..dd65915538fd --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/api.yml @@ -0,0 +1,4 @@ +name: exhaustive +auth: bearer +error-discrimination: + strategy: status-code diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/container.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/container.yml new file mode 100644 index 000000000000..e5a464352bf0 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/container.yml @@ -0,0 +1,55 @@ +imports: + objects: ../types/object.yml + unions: ../types/union.yml + +service: + auth: true + base-path: /container + endpoints: + getAndReturnListOfPrimitives: + path: /list-of-primitives + method: POST + request: list + response: list + + getAndReturnListOfObjects: + path: /list-of-objects + method: POST + request: list + response: list + + getAndReturnSetOfPrimitives: + path: /set-of-primitives + method: POST + request: set + response: set + + getAndReturnSetOfObjects: + path: /set-of-objects + method: POST + request: set + response: set + + getAndReturnMapPrimToPrim: + path: /map-prim-to-prim + method: POST + request: map + response: map + + getAndReturnMapOfPrimToObject: + path: /map-prim-to-object + method: POST + request: map + response: map + + getAndReturnMapOfPrimToUndiscriminatedUnion: + path: /map-prim-to-union + method: POST + request: map + response: map + + getAndReturnOptional: + path: /opt-objects + method: POST + request: optional + response: optional diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/content-type.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/content-type.yml new file mode 100644 index 000000000000..7c54e39fa5a4 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/content-type.yml @@ -0,0 +1,19 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /foo + endpoints: + postJsonPatchContentType: + path: /bar + method: POST + request: + body: objects.ObjectWithOptionalField + content-type: application/json-patch+json + postJsonPatchContentWithCharsetType: + path: /baz + method: POST + request: + body: objects.ObjectWithOptionalField + content-type: application/json-patch+json; charset=utf-8 diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-a.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-a.yml new file mode 100644 index 000000000000..1c860ba5455e --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-a.yml @@ -0,0 +1,39 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /duplicate-names-a + endpoints: + create: + docs: Create endpoint for service A + path: "" + method: POST + request: + name: CreateRequestA + body: + properties: + name: string + value: integer + response: objects.ObjectWithOptionalField + + get: + docs: Get endpoint for service A + path: /{id} + method: GET + request: + name: GetRequestA + path-parameters: + id: string + query-parameters: + filter: optional + + list: + docs: List endpoint for service A + path: "" + method: GET + request: + name: ListRequestA + query-parameters: + page: optional + limit: optional diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-b.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-b.yml new file mode 100644 index 000000000000..910d802b9253 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-b.yml @@ -0,0 +1,39 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /duplicate-names-b + endpoints: + create: + docs: Create endpoint for service B + path: "" + method: POST + request: + name: CreateRequestB + body: + properties: + description: string + count: integer + response: objects.ObjectWithOptionalField + + get: + docs: Get endpoint for service B + path: /{id} + method: GET + request: + name: GetRequestB + path-parameters: + id: string + query-parameters: + expand: optional + + list: + docs: List endpoint for service B + path: "" + method: GET + request: + name: ListRequestB + query-parameters: + cursor: optional + size: optional diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-c.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-c.yml new file mode 100644 index 000000000000..8a7d4573f501 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/duplicate-names-c.yml @@ -0,0 +1,39 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /duplicate-names-c + endpoints: + create: + docs: Create endpoint for service C + path: "" + method: POST + request: + name: CreateRequestC + body: + properties: + label: string + priority: integer + response: objects.ObjectWithOptionalField + + get: + docs: Get endpoint for service C + path: /{id} + method: GET + request: + name: GetRequestC + path-parameters: + id: string + query-parameters: + verbose: optional + + list: + docs: List endpoint for service C + path: "" + method: GET + request: + name: ListRequestC + query-parameters: + offset: optional + count: optional diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/enum.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/enum.yml new file mode 100644 index 000000000000..335a0889cc76 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/enum.yml @@ -0,0 +1,12 @@ +imports: + enums: ../types/enum.yml + +service: + auth: true + base-path: /enum + endpoints: + getAndReturnEnum: + method: POST + path: "" + request: enums.WeatherReport + response: enums.WeatherReport diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/http-methods.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/http-methods.yml new file mode 100644 index 000000000000..8277209b9bbc --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/http-methods.yml @@ -0,0 +1,49 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /http-methods + + endpoints: + testGet: + method: GET + path: /{id} + path-parameters: + id: string + response: string + + testPost: + method: POST + path: "" + request: objects.ObjectWithRequiredField + response: objects.ObjectWithOptionalField + availability: deprecated + + testPut: + method: PUT + path: /{id} + path-parameters: + id: string + request: objects.ObjectWithRequiredField + response: objects.ObjectWithOptionalField + availability: + status: deprecated + message: Use testPatch instead. + + testPatch: + method: PATCH + path: /{id} + path-parameters: + id: string + request: objects.ObjectWithOptionalField + response: objects.ObjectWithOptionalField + availability: pre-release + + testDelete: + method: DELETE + path: /{id} + path-parameters: + id: string + response: boolean + availability: in-development diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/object.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/object.yml new file mode 100644 index 000000000000..2c3ef2535550 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/object.yml @@ -0,0 +1,94 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /object + endpoints: + getAndReturnWithOptionalField: + path: /get-and-return-with-optional-field + method: POST + request: objects.ObjectWithOptionalField + response: objects.ObjectWithOptionalField + # Temporarily commented out - nested long + # examples: + # - name: WithLargeInteger + # request: + # string: "test" + # integer: 21991583578 # Large value that exceeds Integer.MAX_VALUE (2147483647) + # long: 9223372036854775807 + # double: 3.14 + # bool: true + # response: + # body: + # string: "test" + # integer: 21991583578 # This tests our integer overflow fix + # long: 9223372036854775807 + # double: 3.14 + # bool: true + + getAndReturnWithRequiredField: + path: /get-and-return-with-required-field + method: POST + request: objects.ObjectWithRequiredField + response: objects.ObjectWithRequiredField + + getAndReturnWithMapOfMap: + path: /get-and-return-with-map-of-map + method: POST + request: objects.ObjectWithMapOfMap + response: objects.ObjectWithMapOfMap + + getAndReturnNestedWithOptionalField: + path: /get-and-return-nested-with-optional-field + method: POST + request: objects.NestedObjectWithOptionalField + response: objects.NestedObjectWithOptionalField + + getAndReturnNestedWithRequiredField: + path: /get-and-return-nested-with-required-field/{string} + method: POST + path-parameters: + string: string + request: objects.NestedObjectWithRequiredField + response: objects.NestedObjectWithRequiredField + + getAndReturnNestedWithRequiredFieldAsList: + path: /get-and-return-nested-with-required-field-list + method: POST + request: list + response: objects.NestedObjectWithRequiredField + + getAndReturnWithUnknownField: + path: /get-and-return-with-unknown-field + method: POST + request: objects.ObjectWithUnknownField + response: objects.ObjectWithUnknownField + examples: + - name: BackslashExample + request: + unknown: + "\\$ref": "https://example.com/schema" + response: + body: + unknown: + "\\$ref": "https://example.com/schema" + + getAndReturnWithDatetimeLikeString: + docs: | + Tests that string fields containing datetime-like values are NOT reformatted. + The datetimeLikeString field should preserve its exact value "2023-08-31T14:15:22Z" + without being converted to "2023-08-31T14:15:22.000Z". + path: /get-and-return-with-datetime-like-string + method: POST + request: objects.ObjectWithDatetimeLikeString + response: objects.ObjectWithDatetimeLikeString + examples: + - name: DatetimeLikeStringExample + request: + datetimeLikeString: "2023-08-31T14:15:22Z" + actualDatetime: "2023-08-31T14:15:22Z" + response: + body: + datetimeLikeString: "2023-08-31T14:15:22Z" + actualDatetime: "2023-08-31T14:15:22Z" diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/pagination.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/pagination.yml new file mode 100644 index 000000000000..b4fb23836f17 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/pagination.yml @@ -0,0 +1,31 @@ +imports: + objects: ../types/object.yml + +types: + PaginatedResponse: + properties: + items: list + next: optional + +service: + auth: true + base-path: /pagination + endpoints: + listItems: + docs: List items with cursor pagination + pagination: + cursor: $request.cursor + next_cursor: $response.next + results: $response.items + method: GET + path: "" + request: + name: ListItemsRequest + query-parameters: + cursor: + type: optional + docs: The cursor for pagination + limit: + type: optional + docs: Maximum number of items to return + response: PaginatedResponse diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/params.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/params.yml new file mode 100644 index 000000000000..ef1f29865078 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/params.yml @@ -0,0 +1,110 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /params + endpoints: + getWithPath: + docs: GET with path param + path: /path/{param} + path-parameters: + param: string + method: GET + response: string + + getWithInlinePath: + docs: GET with path param + path: /path/{param} + method: GET + response: string + request: + name: getWithInlinePath + path-parameters: + param: string + + getWithQuery: + docs: GET with query param + path: "" + method: GET + request: + name: GetWithQuery + query-parameters: + query: string #mandatory for test + number: integer + + getWithAllowMultipleQuery: + docs: GET with multiple of same query param + path: "" + method: GET + request: + name: GetWithMultipleQuery + query-parameters: + query: + type: string + allow-multiple: true + number: + type: integer + allow-multiple: true + + getWithPathAndQuery: + docs: GET with path and query params + path: /path-query/{param} + method: GET + path-parameters: + param: string + request: + name: GetWithPathAndQuery + query-parameters: + query: string #mandatory for test + + getWithInlinePathAndQuery: + docs: GET with path and query params + path: /path-query/{param} + method: GET + request: + name: getWithInlinePathAndQuery + query-parameters: + query: string #mandatory for test + path-parameters: + param: string + + modifyWithPath: + docs: PUT to update with path param + path: /path/{param} + path-parameters: + param: string + method: PUT + request: + name: ModifyResourceAtPath + body: string + response: string + + modifyWithInlinePath: + docs: PUT to update with path param + path: /path/{param} + method: PUT + request: + name: ModifyResourceAtInlinedPath + body: string + path-parameters: + param: string + response: string + + uploadWithPath: + docs: POST bytes with path param returning object + path: /path/{param} + path-parameters: + param: string + method: POST + request: + name: UploadWithPath + body: bytes + response: objects.ObjectWithRequiredField + examples: + - path-parameters: + param: "upload-path" + request: "bytes-content" + response: + body: + string: "uploaded" diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/primitive.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/primitive.yml new file mode 100644 index 000000000000..8dd7674164c8 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/primitive.yml @@ -0,0 +1,60 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /primitive + endpoints: + getAndReturnString: + path: /string + method: POST + request: string + response: string + + getAndReturnInt: + path: /integer + method: POST + request: integer + response: integer + + getAndReturnLong: + path: /long + method: POST + request: long + response: long + + getAndReturnDouble: + path: /double + method: POST + request: double + response: double + + getAndReturnBool: + path: /boolean + method: POST + request: boolean + response: boolean + + getAndReturnDatetime: + path: /datetime + method: POST + request: datetime + response: datetime + + getAndReturnDate: + path: /date + method: POST + request: date + response: date + + getAndReturnUUID: + path: /uuid + method: POST + request: uuid + response: uuid + + getAndReturnBase64: + path: /base64 + method: POST + request: base64 + response: base64 diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/put.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/put.yml new file mode 100644 index 000000000000..1ff92a62026d --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/put.yml @@ -0,0 +1,45 @@ +types: + Error: + properties: + category: ErrorCategory + code: ErrorCode + detail: optional + field: optional + ErrorCategory: + enum: + - API_ERROR + - AUTHENTICATION_ERROR + - INVALID_REQUEST_ERROR + ErrorCode: + enum: + - INTERNAL_SERVER_ERROR + - UNAUTHORIZED + - FORBIDDEN + - BAD_REQUEST + - CONFLICT + - GONE + - UNPROCESSABLE_ENTITY + - NOT_IMPLEMENTED + - BAD_GATEWAY + - SERVICE_UNAVAILABLE + - Unknown + PutResponse: + properties: + errors: optional> + +service: + auth: false + base-path: "" + endpoints: + add: + auth: true + display-name: Put + method: PUT + path: /{id} + request: + name: PutRequest + path-parameters: + id: string + response: + status-code: 200 + type: PutResponse diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/union.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/union.yml new file mode 100644 index 000000000000..ce9021160d7b --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/union.yml @@ -0,0 +1,12 @@ +imports: + unions: ../types/union.yml + +service: + auth: true + base-path: /union + endpoints: + getAndReturnUnion: + method: POST + path: "" + request: unions.Animal + response: unions.Animal diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/urls.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/urls.yml new file mode 100644 index 000000000000..37c4b63e7113 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/endpoints/urls.yml @@ -0,0 +1,25 @@ +service: + auth: true + base-path: /urls + + endpoints: + withMixedCase: + method: GET + path: /MixedCase + response: string + noEndingSlash: + method: GET + path: /no-ending-slash + response: string + withEndingSlash: + method: GET + path: /with-ending-slash/ + response: string + withUnderscores: + method: GET + path: /with_underscores + response: string + # withSpecialChars: + # method: GET + # path: /tilde~(parentheses)@at@;semicolon;,comma,=equals= + # response: string diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/general-errors.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/general-errors.yml new file mode 100644 index 000000000000..5fbf9cfc4173 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/general-errors.yml @@ -0,0 +1,9 @@ +errors: + BadRequestBody: + status-code: 400 + type: BadObjectRequestInfo + +types: + BadObjectRequestInfo: + properties: + message: string diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/inlined-requests.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/inlined-requests.yml new file mode 100644 index 000000000000..9347fe7e335d --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/inlined-requests.yml @@ -0,0 +1,25 @@ +imports: + objects: ./types/object.yml + errors: ./general-errors.yml + +# test req bodies, path params, query params, multiple query params, etc. +# test union and enum as well + +service: + auth: false + base-path: /req-bodies + endpoints: + postWithObjectBodyandResponse: + docs: POST with custom object in request body, response is an object + path: /object + method: POST + request: + name: PostWithObjectBody + body: + properties: + string: string + integer: integer + NestedObject: objects.ObjectWithOptionalField + response: objects.ObjectWithOptionalField + errors: + - errors.BadRequestBody diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/no-auth.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/no-auth.yml new file mode 100644 index 000000000000..e3c33ed7fabf --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/no-auth.yml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +imports: + general-errors: ./general-errors.yml + +service: + auth: false + base-path: /no-auth + endpoints: + postWithNoAuth: + auth: false + docs: POST request with no auth + path: "" + method: POST + request: + name: PostWithNoAuth + body: unknown + response: boolean + errors: + - general-errors.BadRequestBody diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/no-req-body.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/no-req-body.yml new file mode 100644 index 000000000000..daffd9a495cd --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/no-req-body.yml @@ -0,0 +1,16 @@ +imports: + objects: ./types/object.yml + +service: + auth: true + base-path: /no-req-body + endpoints: + getWithNoRequestBody: + path: "" + method: GET + response: objects.ObjectWithOptionalField + + postWithNoRequestBody: + path: "" + method: POST + response: string diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/req-with-headers.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/req-with-headers.yml new file mode 100644 index 000000000000..9e49725782f3 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/req-with-headers.yml @@ -0,0 +1,14 @@ +service: + base-path: /test-headers + auth: true + headers: + X-TEST-SERVICE-HEADER: string + endpoints: + getWithCustomHeader: + path: /custom-header + method: POST + request: + name: ReqWithHeaders + headers: + X-TEST-ENDPOINT-HEADER: string + body: string diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/types/docs.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/docs.yml new file mode 100644 index 000000000000..5daf08a5ea90 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/docs.yml @@ -0,0 +1,70 @@ +types: + ObjectWithDocs: + properties: + string: + type: string + docs: | + Characters that could lead to broken generated SDKs: + + Markdown Escapes: + - \_: Escaped underscore (e.g., FOO\_BAR) + - \*: Escaped asterisk + + JSDoc (JavaScript/TypeScript): + - @: Used for JSDoc tags + - {: }: Used for type definitions + - <: >: HTML tags + - *: Can interfere with comment blocks + - /**: JSDoc comment start + - **/: JSDoc comment end + - &: HTML entities + + XMLDoc (C#): + - <: >: XML tags + - &: ': ": <: >: XML special characters + - {: }: Used for interpolated strings + - ///: Comment marker + - /**: Block comment start + - **/: Block comment end + + XMLDoc (C#) (Example of actual XML tags): + See the docs for more info. + Use getValue() to retrieve the value. + Note: when count < 10 or count > 100, special handling applies. + + Javadoc (Java): + - @: Used for Javadoc tags + - <: >: HTML tags + - &: HTML entities + - *: Can interfere with comment blocks + - /**: Javadoc comment start + - **/: Javadoc comment end + + Doxygen (C++): + - \: Used for Doxygen commands + - @: Alternative command prefix + - <: >: XML/HTML tags + - &: HTML entities + - /**: C-style comment start + - **/: C-style comment end + + RDoc (Ruby): + - :: Used in symbol notation + - =: Section markers + - #: Comment marker + - =begin: Block comment start + - =end: Block comment end + - @: Instance variable prefix + - $: Global variable prefix + - %: String literal delimiter + - #{: String interpolation start + - }: String interpolation end + + PHPDoc (PHP): + - @: Used for PHPDoc tags + - {: }: Used for type definitions + - $: Variable prefix + - /**: PHPDoc comment start + - **/: PHPDoc comment end + - *: Can interfere with comment blocks + - &: HTML entities diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/types/enum.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/enum.yml new file mode 100644 index 000000000000..a90686092e93 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/enum.yml @@ -0,0 +1,12 @@ +types: + WeatherReport: + enum: + - SUNNY + - CLOUDY + - RAINING + - SNOWING + +errors: + ErrorWithEnumBody: + status-code: 400 + type: WeatherReport #does this even make sense? the type of the error body would be enum, and it could only be one of the 4 values? diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/types/object.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/object.yml new file mode 100644 index 000000000000..86ddd5f1390b --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/object.yml @@ -0,0 +1,79 @@ +types: + ObjectWithOptionalField: #generic object that supports any type, makes it easier to use when testing + properties: + string: + type: optional + docs: This is a rather long descriptor of this single field in a more complex type. If you ask me I think this is a pretty good description for this field all things considered. + integer: optional + long: optional + double: optional + bool: optional + datetime: optional + date: optional + uuid: optional + base64: optional + list: optional> + set: optional> + map: optional> + bigint: optional + + ObjectWithRequiredField: + properties: + string: string + + ObjectWithMapOfMap: + properties: + map: map> + + NestedObjectWithOptionalField: + properties: + string: optional + NestedObject: optional + + NestedObjectWithRequiredField: + properties: + string: string + NestedObject: ObjectWithOptionalField + + DoubleOptional: + properties: + optionalAlias: optional + + OptionalAlias: optional + + ObjectWithDatetimeLikeString: + docs: | + This type tests that string fields containing datetime-like values + are NOT reformatted by the wire test generator. The string field + should preserve its exact value even if it looks like a datetime. + properties: + datetimeLikeString: + type: string + docs: A string field that happens to contain a datetime-like value + actualDatetime: + type: datetime + docs: An actual datetime field for comparison + + ObjectWithUnknownField: + docs: | + Tests that unknown/any values containing backslashes in map keys + are properly escaped in Go string literals. + properties: + unknown: unknown + +errors: + ObjectWithOptionalFieldError: + status-code: 400 + type: ObjectWithOptionalField + + ObjectWithRequiredFieldError: + status-code: 400 + type: ObjectWithRequiredField + + NestedObjectWithOptionalFieldError: + status-code: 400 + type: NestedObjectWithOptionalField + + NestedObjectWithRequiredFieldError: + status-code: 400 + type: NestedObjectWithRequiredField diff --git a/test-definitions/fern/apis/go-deterministic-ordering/definition/types/union.yml b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/union.yml new file mode 100644 index 000000000000..f28f12250be5 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/definition/types/union.yml @@ -0,0 +1,29 @@ +types: + Animal: + discriminant: animal + union: + dog: Dog + cat: Cat + + Dog: + properties: + name: string + likesToWoof: boolean + + Cat: + properties: + name: string + likesToMeow: boolean + + MixedType: + discriminated: false + union: + - double + - boolean + - string + - list + +errors: + ErrorWithUnionBody: + status-code: 400 + type: Animal #has to send either dog or cat object in error body diff --git a/test-definitions/fern/apis/go-deterministic-ordering/generators.yml b/test-definitions/fern/apis/go-deterministic-ordering/generators.yml new file mode 100644 index 000000000000..449219c298d7 --- /dev/null +++ b/test-definitions/fern/apis/go-deterministic-ordering/generators.yml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +groups: + go-sdk: + generators: + - name: fernapi/fern-go-sdk + version: latest + ir-version: v61 + config: + union: v1 + enableWireTests: true + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/go-sdk-tests + branch: go-deterministic-ordering From 9d05fa6a5c3b3adb2d9cc862652be9655fbd9516 Mon Sep 17 00:00:00 2001 From: Naman Anand Date: Sat, 14 Mar 2026 01:30:20 +0530 Subject: [PATCH 16/29] fix(java): Enable forward-compatible enums; stop extra props (#13524) * Enable forward-compatible enums; stop extra props Add support for forward-compatible enums and disable deserialization of additional properties. A new config flag enable-forward-compatible-enums (default true) was added to JavaSdkDownloadFilesCustomConfig and passed through the CLI to enable the feature. ForwardCompatibleEnumGenerator's constructor was made public. ClientGeneratorContext.deserializeWithAdditionalProperties() was changed to return false to prevent automatic acceptance of unknown properties during deserialization. * Make enum ctor package-private; enable addl props Remove the public modifier from the forward-compatible enum constructor to make it package-private and limit external instantiation. Also change ClientGeneratorContext.deserializeWithAdditionalProperties() to return true so generated clients will deserialize additional/unknown properties. * Add Java SDK v3.44.1 changelog entry Add a 3.44.1 entry documenting a fix: the enable-forward-compatible-enums config flag was ignored in download files mode because it was missing from JavaSdkDownloadFilesCustomConfig, so setting enable-forward-compatible-enums: false in generators.yml had no effect when using --local generation. Also records createdAt (2026-03-14) and irVersion (65). --------- Co-authored-by: Naman Anand --- .../sdk/src/main/java/com/fern/java/client/Cli.java | 1 + .../java/client/JavaSdkDownloadFilesCustomConfig.java | 6 ++++++ generators/java/sdk/versions.yml | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java b/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java index b447cd1760cc..87d870d7e00f 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java @@ -180,6 +180,7 @@ public void runInDownloadFilesModeHook( .gradleCentralDependencyManagement(customConfig.gradleCentralDependencyManagement()) .customInterceptors(customConfig.customInterceptors()) .customPlugins(customConfig.customPlugins()) + .enableForwardCompatibleEnum(customConfig.enableForwardCompatibleEnums()) .build(); Boolean generateFullProject = ir.getPublishConfig() diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/JavaSdkDownloadFilesCustomConfig.java b/generators/java/sdk/src/main/java/com/fern/java/client/JavaSdkDownloadFilesCustomConfig.java index 4c963622bb85..1cee788d8cf6 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/JavaSdkDownloadFilesCustomConfig.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/JavaSdkDownloadFilesCustomConfig.java @@ -71,6 +71,12 @@ default Boolean customInterceptors() { return false; } + @Value.Default + @JsonProperty("enable-forward-compatible-enums") + default Boolean enableForwardCompatibleEnums() { + return true; + } + static ImmutableJavaSdkDownloadFilesCustomConfig.Builder builder() { return ImmutableJavaSdkDownloadFilesCustomConfig.builder(); } diff --git a/generators/java/sdk/versions.yml b/generators/java/sdk/versions.yml index 6b78599be28b..b1e12ac2cca9 100644 --- a/generators/java/sdk/versions.yml +++ b/generators/java/sdk/versions.yml @@ -1,4 +1,15 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.44.1 + changelogEntry: + - summary: | + Fix `enable-forward-compatible-enums` config flag being ignored in + download files mode. The flag was missing from + `JavaSdkDownloadFilesCustomConfig`, so `enable-forward-compatible-enums: false` + in generators.yml had no effect when using `--local` generation. + type: fix + createdAt: "2026-03-14" + irVersion: 65 + - version: 3.44.0 changelogEntry: - summary: | From f4a1866e19b45fee34ec6315d9dd7400fbcbe9a0 Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:06:56 -0400 Subject: [PATCH 17/29] chore(go): update go-model seed (#13535) Co-authored-by: patrickthornton --- .../.fern/metadata.json | 7 + .../go-model/go-deterministic-ordering/doc.go | 1 + .../endpoints/pagination.go | 69 ++ .../endpoints/put.go | 209 +++++++ .../general_errors.go | 60 ++ .../go-model/go-deterministic-ordering/go.mod | 16 + .../go-model/go-deterministic-ordering/go.sum | 12 + .../internal/extra_properties.go | 141 +++++ .../internal/extra_properties_test.go | 228 +++++++ .../internal/stringer.go | 13 + .../internal/time.go | 165 +++++ .../go-deterministic-ordering/snippet.json | 0 .../go-deterministic-ordering/types/docs.go | 124 ++++ .../go-deterministic-ordering/types/enum.go | 35 ++ .../go-deterministic-ordering/types/object.go | 589 ++++++++++++++++++ .../go-deterministic-ordering/types/union.go | 140 +++++ 16 files changed, 1809 insertions(+) create mode 100644 seed/go-model/go-deterministic-ordering/.fern/metadata.json create mode 100644 seed/go-model/go-deterministic-ordering/doc.go create mode 100644 seed/go-model/go-deterministic-ordering/endpoints/pagination.go create mode 100644 seed/go-model/go-deterministic-ordering/endpoints/put.go create mode 100644 seed/go-model/go-deterministic-ordering/general_errors.go create mode 100644 seed/go-model/go-deterministic-ordering/go.mod create mode 100644 seed/go-model/go-deterministic-ordering/go.sum create mode 100644 seed/go-model/go-deterministic-ordering/internal/extra_properties.go create mode 100644 seed/go-model/go-deterministic-ordering/internal/extra_properties_test.go create mode 100644 seed/go-model/go-deterministic-ordering/internal/stringer.go create mode 100644 seed/go-model/go-deterministic-ordering/internal/time.go create mode 100644 seed/go-model/go-deterministic-ordering/snippet.json create mode 100644 seed/go-model/go-deterministic-ordering/types/docs.go create mode 100644 seed/go-model/go-deterministic-ordering/types/enum.go create mode 100644 seed/go-model/go-deterministic-ordering/types/object.go create mode 100644 seed/go-model/go-deterministic-ordering/types/union.go diff --git a/seed/go-model/go-deterministic-ordering/.fern/metadata.json b/seed/go-model/go-deterministic-ordering/.fern/metadata.json new file mode 100644 index 000000000000..ace9aefefa3d --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/.fern/metadata.json @@ -0,0 +1,7 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-go-model", + "generatorVersion": "latest", + "originGitCommit": "DUMMY", + "sdkVersion": "v0.0.1" +} \ No newline at end of file diff --git a/seed/go-model/go-deterministic-ordering/doc.go b/seed/go-model/go-deterministic-ordering/doc.go new file mode 100644 index 000000000000..a00b53a61274 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/doc.go @@ -0,0 +1 @@ +package exhaustive \ No newline at end of file diff --git a/seed/go-model/go-deterministic-ordering/endpoints/pagination.go b/seed/go-model/go-deterministic-ordering/endpoints/pagination.go new file mode 100644 index 000000000000..f0374977de27 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/endpoints/pagination.go @@ -0,0 +1,69 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + types "github.com/go-deterministic-ordering/fern/types" +) + +type PaginatedResponse struct { + Items []*types.ObjectWithRequiredField `json:"items" url:"items"` + Next *string `json:"next,omitempty" url:"next,omitempty"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (p *PaginatedResponse) GetItems() []*types.ObjectWithRequiredField { + if p == nil { + return nil + } + return p.Items +} + +func (p *PaginatedResponse) GetNext() *string { + if p == nil { + return nil + } + return p.Next +} + +func (p *PaginatedResponse) GetExtraProperties() map[string]any { + if p == nil { + return nil + } + return p.extraProperties +} + +func (p *PaginatedResponse) UnmarshalJSON( + data []byte, +) error { + type unmarshaler PaginatedResponse + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *p = PaginatedResponse(value) + extraProperties, err := internal.ExtractExtraProperties(data, *p) + if err != nil { + return err + } + p.extraProperties = extraProperties + p.rawJSON = json.RawMessage(data) + return nil +} + +func (p *PaginatedResponse) String() string { + if len(p.rawJSON) > 0 { + if value, err := internal.StringifyJSON(p.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(p); err == nil { + return value + } + return fmt.Sprintf("%#v", p) +} diff --git a/seed/go-model/go-deterministic-ordering/endpoints/put.go b/seed/go-model/go-deterministic-ordering/endpoints/put.go new file mode 100644 index 000000000000..38bfeb94dd5f --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/endpoints/put.go @@ -0,0 +1,209 @@ +// Code generated by Fern. DO NOT EDIT. + +package endpoints + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" +) + +type Error struct { + Category *ErrorCategory `json:"category" url:"category"` + Code *ErrorCode `json:"code" url:"code"` + Detail *string `json:"detail,omitempty" url:"detail,omitempty"` + Field *string `json:"field,omitempty" url:"field,omitempty"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (e *Error) GetCategory() *ErrorCategory { + if e == nil { + return nil + } + return e.Category +} + +func (e *Error) GetCode() *ErrorCode { + if e == nil { + return nil + } + return e.Code +} + +func (e *Error) GetDetail() *string { + if e == nil { + return nil + } + return e.Detail +} + +func (e *Error) GetField() *string { + if e == nil { + return nil + } + return e.Field +} + +func (e *Error) GetExtraProperties() map[string]any { + if e == nil { + return nil + } + return e.extraProperties +} + +func (e *Error) UnmarshalJSON( + data []byte, +) error { + type unmarshaler Error + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *e = Error(value) + extraProperties, err := internal.ExtractExtraProperties(data, *e) + if err != nil { + return err + } + e.extraProperties = extraProperties + e.rawJSON = json.RawMessage(data) + return nil +} + +func (e *Error) String() string { + if len(e.rawJSON) > 0 { + if value, err := internal.StringifyJSON(e.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} + +type ErrorCategory string + +const ( + ErrorCategoryApiError = "API_ERROR" + ErrorCategoryAuthenticationError = "AUTHENTICATION_ERROR" + ErrorCategoryInvalidRequestError = "INVALID_REQUEST_ERROR" +) + +func NewErrorCategoryFromString(s string) (ErrorCategory, error) { + switch s { + case "API_ERROR": + return ErrorCategoryApiError, nil + case "AUTHENTICATION_ERROR": + return ErrorCategoryAuthenticationError, nil + case "INVALID_REQUEST_ERROR": + return ErrorCategoryInvalidRequestError, nil + } + var t ErrorCategory + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (e ErrorCategory) Ptr() *ErrorCategory { + return &e +} + +type ErrorCode string + +const ( + ErrorCodeInternalServerError = "INTERNAL_SERVER_ERROR" + ErrorCodeUnauthorized = "UNAUTHORIZED" + ErrorCodeForbidden = "FORBIDDEN" + ErrorCodeBadRequest = "BAD_REQUEST" + ErrorCodeConflict = "CONFLICT" + ErrorCodeGone = "GONE" + ErrorCodeUnprocessableEntity = "UNPROCESSABLE_ENTITY" + ErrorCodeNotImplemented = "NOT_IMPLEMENTED" + ErrorCodeBadGateway = "BAD_GATEWAY" + ErrorCodeServiceUnavailable = "SERVICE_UNAVAILABLE" + ErrorCodeUnknown = "Unknown" +) + +func NewErrorCodeFromString(s string) (ErrorCode, error) { + switch s { + case "INTERNAL_SERVER_ERROR": + return ErrorCodeInternalServerError, nil + case "UNAUTHORIZED": + return ErrorCodeUnauthorized, nil + case "FORBIDDEN": + return ErrorCodeForbidden, nil + case "BAD_REQUEST": + return ErrorCodeBadRequest, nil + case "CONFLICT": + return ErrorCodeConflict, nil + case "GONE": + return ErrorCodeGone, nil + case "UNPROCESSABLE_ENTITY": + return ErrorCodeUnprocessableEntity, nil + case "NOT_IMPLEMENTED": + return ErrorCodeNotImplemented, nil + case "BAD_GATEWAY": + return ErrorCodeBadGateway, nil + case "SERVICE_UNAVAILABLE": + return ErrorCodeServiceUnavailable, nil + case "Unknown": + return ErrorCodeUnknown, nil + } + var t ErrorCode + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (e ErrorCode) Ptr() *ErrorCode { + return &e +} + +type PutResponse struct { + Errors []*Error `json:"errors,omitempty" url:"errors,omitempty"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (p *PutResponse) GetErrors() []*Error { + if p == nil { + return nil + } + return p.Errors +} + +func (p *PutResponse) GetExtraProperties() map[string]any { + if p == nil { + return nil + } + return p.extraProperties +} + +func (p *PutResponse) UnmarshalJSON( + data []byte, +) error { + type unmarshaler PutResponse + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *p = PutResponse(value) + extraProperties, err := internal.ExtractExtraProperties(data, *p) + if err != nil { + return err + } + p.extraProperties = extraProperties + p.rawJSON = json.RawMessage(data) + return nil +} + +func (p *PutResponse) String() string { + if len(p.rawJSON) > 0 { + if value, err := internal.StringifyJSON(p.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(p); err == nil { + return value + } + return fmt.Sprintf("%#v", p) +} diff --git a/seed/go-model/go-deterministic-ordering/general_errors.go b/seed/go-model/go-deterministic-ordering/general_errors.go new file mode 100644 index 000000000000..64216dfd0306 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/general_errors.go @@ -0,0 +1,60 @@ +// Code generated by Fern. DO NOT EDIT. + +package exhaustive + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" +) + +type BadObjectRequestInfo struct { + Message string `json:"message" url:"message"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (b *BadObjectRequestInfo) GetMessage() string { + if b == nil { + return "" + } + return b.Message +} + +func (b *BadObjectRequestInfo) GetExtraProperties() map[string]any { + if b == nil { + return nil + } + return b.extraProperties +} + +func (b *BadObjectRequestInfo) UnmarshalJSON( + data []byte, +) error { + type unmarshaler BadObjectRequestInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *b = BadObjectRequestInfo(value) + extraProperties, err := internal.ExtractExtraProperties(data, *b) + if err != nil { + return err + } + b.extraProperties = extraProperties + b.rawJSON = json.RawMessage(data) + return nil +} + +func (b *BadObjectRequestInfo) String() string { + if len(b.rawJSON) > 0 { + if value, err := internal.StringifyJSON(b.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(b); err == nil { + return value + } + return fmt.Sprintf("%#v", b) +} diff --git a/seed/go-model/go-deterministic-ordering/go.mod b/seed/go-model/go-deterministic-ordering/go.mod new file mode 100644 index 000000000000..3e6c610a55fb --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/go.mod @@ -0,0 +1,16 @@ +module github.com/go-deterministic-ordering/fern + +go 1.21 + +toolchain go1.23.8 + +require github.com/google/uuid v1.6.0 + +require github.com/stretchr/testify v1.8.4 + +require gopkg.in/yaml.v3 v3.0.1 // indirect + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/seed/go-model/go-deterministic-ordering/go.sum b/seed/go-model/go-deterministic-ordering/go.sum new file mode 100644 index 000000000000..fcca6d128057 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seed/go-model/go-deterministic-ordering/internal/extra_properties.go b/seed/go-model/go-deterministic-ordering/internal/extra_properties.go new file mode 100644 index 000000000000..57517691f132 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/internal/extra_properties.go @@ -0,0 +1,141 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler any, key string, value any) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]any{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler any, extraProperties map[string]any) ([]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 any, exclude ...string) (map[string]any, 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]any + 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 any) ([]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-model/go-deterministic-ordering/internal/extra_properties_test.go b/seed/go-model/go-deterministic-ordering/internal/extra_properties_test.go new file mode 100644 index 000000000000..0d46257763fb --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/internal/extra_properties_test.go @@ -0,0 +1,228 @@ +package internal + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler any + giveExtraProperties map[string]any + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]any{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]any{42: "value"}, + giveExtraProperties: map[string]any{"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]any{"key": "value"}, + giveExtraProperties: map[string]any{"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]any{"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]any{"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]any{}, + giveExtraProperties: map[string]any{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]any{"key": "value"}, + giveExtraProperties: map[string]any{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]any{}, + giveExtraProperties: map[string]any{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]any{"key": "value"}, + giveExtraProperties: map[string]any{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]any{"key": "value"}, + giveExtraProperties: map[string]any{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]any{"key": "value"}, + giveExtraProperties: map[string]any{ + "user": map[string]any{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]any{"key": "value"}, + giveExtraProperties: map[string]any{ + "metadata": map[string]any{ + "ip": "127.0.0.1", + }, + "user": map[string]any{ + "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]any{ + "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]any) + 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]any{"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]any{"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]any{"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-model/go-deterministic-ordering/internal/stringer.go b/seed/go-model/go-deterministic-ordering/internal/stringer.go new file mode 100644 index 000000000000..0be54d1b5359 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/internal/stringer.go @@ -0,0 +1,13 @@ +package internal + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value any) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/seed/go-model/go-deterministic-ordering/internal/time.go b/seed/go-model/go-deterministic-ordering/internal/time.go new file mode 100644 index 000000000000..57f901a35ed8 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/internal/time.go @@ -0,0 +1,165 @@ +package internal + +import ( + "encoding/json" + "fmt" + "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 { + // If the value is not a string, check if it is a number (unix epoch seconds). + var epoch int64 + if numErr := json.Unmarshal(data, &epoch); numErr == nil { + t := time.Unix(epoch, 0).UTC() + *d = DateTime{t: &t} + return nil + } + return err + } + + // Try RFC3339Nano first (superset of RFC3339, supports fractional seconds). + parsedTime, err := time.Parse(time.RFC3339Nano, raw) + if err == nil { + *d = DateTime{t: &parsedTime} + return nil + } + rfc3339NanoErr := err + + // Fall back to ISO 8601 without timezone (assume UTC). + parsedTime, err = time.Parse("2006-01-02T15:04:05", raw) + if err == nil { + parsedTime = parsedTime.UTC() + *d = DateTime{t: &parsedTime} + return nil + } + iso8601Err := err + + // Fall back to date-only format. + parsedTime, err = time.Parse("2006-01-02", raw) + if err == nil { + parsedTime = parsedTime.UTC() + *d = DateTime{t: &parsedTime} + return nil + } + dateOnlyErr := err + + return fmt.Errorf("unable to parse datetime string %q: tried RFC3339Nano (%v), ISO8601 (%v), date-only (%v)", raw, rfc3339NanoErr, iso8601Err, dateOnlyErr) +} diff --git a/seed/go-model/go-deterministic-ordering/snippet.json b/seed/go-model/go-deterministic-ordering/snippet.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/seed/go-model/go-deterministic-ordering/types/docs.go b/seed/go-model/go-deterministic-ordering/types/docs.go new file mode 100644 index 000000000000..49c06a5436d5 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/types/docs.go @@ -0,0 +1,124 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" +) + +type ObjectWithDocs struct { + // Characters that could lead to broken generated SDKs: + // + // Markdown Escapes: + // - \_: Escaped underscore (e.g., FOO\_BAR) + // - \*: Escaped asterisk + // + // JSDoc (JavaScript/TypeScript): + // - @: Used for JSDoc tags + // - {: }: Used for type definitions + // - <: >: HTML tags + // - *: Can interfere with comment blocks + // - /**: JSDoc comment start + // - ** /: JSDoc comment end + // - &: HTML entities + // + // XMLDoc (C#): + // - <: >: XML tags + // - &: ': ": <: >: XML special characters + // - {: }: Used for interpolated strings + // - ///: Comment marker + // - /**: Block comment start + // - ** /: Block comment end + // + // XMLDoc (C#) (Example of actual XML tags): + // See the docs for more info. + // Use getValue() to retrieve the value. + // Note: when count < 10 or count > 100, special handling applies. + // + // Javadoc (Java): + // - @: Used for Javadoc tags + // - <: >: HTML tags + // - &: HTML entities + // - *: Can interfere with comment blocks + // - /**: Javadoc comment start + // - ** /: Javadoc comment end + // + // Doxygen (C++): + // - \: Used for Doxygen commands + // - @: Alternative command prefix + // - <: >: XML/HTML tags + // - &: HTML entities + // - /**: C-style comment start + // - ** /: C-style comment end + // + // RDoc (Ruby): + // - :: Used in symbol notation + // - =: Section markers + // - #: Comment marker + // - =begin: Block comment start + // - =end: Block comment end + // - @: Instance variable prefix + // - $: Global variable prefix + // - %: String literal delimiter + // - #{: String interpolation start + // - }: String interpolation end + // + // PHPDoc (PHP): + // - @: Used for PHPDoc tags + // - {: }: Used for type definitions + // - $: Variable prefix + // - /**: PHPDoc comment start + // - ** /: PHPDoc comment end + // - *: Can interfere with comment blocks + // - &: HTML entities + FieldString string `json:"string" url:"string"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (o *ObjectWithDocs) GetFieldString() string { + if o == nil { + return "" + } + return o.FieldString +} + +func (o *ObjectWithDocs) GetExtraProperties() map[string]any { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithDocs) UnmarshalJSON( + data []byte, +) error { + type unmarshaler ObjectWithDocs + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithDocs(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithDocs) String() string { + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} diff --git a/seed/go-model/go-deterministic-ordering/types/enum.go b/seed/go-model/go-deterministic-ordering/types/enum.go new file mode 100644 index 000000000000..782b1d9ce9ce --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/types/enum.go @@ -0,0 +1,35 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + fmt "fmt" +) + +type WeatherReport string + +const ( + WeatherReportSunny = "SUNNY" + WeatherReportCloudy = "CLOUDY" + WeatherReportRaining = "RAINING" + WeatherReportSnowing = "SNOWING" +) + +func NewWeatherReportFromString(s string) (WeatherReport, error) { + switch s { + case "SUNNY": + return WeatherReportSunny, nil + case "CLOUDY": + return WeatherReportCloudy, nil + case "RAINING": + return WeatherReportRaining, nil + case "SNOWING": + return WeatherReportSnowing, nil + } + var t WeatherReport + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (w WeatherReport) Ptr() *WeatherReport { + return &w +} diff --git a/seed/go-model/go-deterministic-ordering/types/object.go b/seed/go-model/go-deterministic-ordering/types/object.go new file mode 100644 index 000000000000..24fa0806290b --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/types/object.go @@ -0,0 +1,589 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" + uuid "github.com/google/uuid" + time "time" +) + +type ObjectWithOptionalField struct { + // This is a rather long descriptor of this single field in a more complex type. If you ask me I think this is a pretty good description for this field all things considered. + FieldString *string `json:"string,omitempty" url:"string,omitempty"` + Integer *int `json:"integer,omitempty" url:"integer,omitempty"` + Long *int64 `json:"long,omitempty" url:"long,omitempty"` + Double *float64 `json:"double,omitempty" url:"double,omitempty"` + Bool *bool `json:"bool,omitempty" url:"bool,omitempty"` + Datetime *time.Time `json:"datetime,omitempty" url:"datetime,omitempty"` + Date *time.Time `json:"date,omitempty" url:"date,omitempty"` + Uuid *uuid.UUID `json:"uuid,omitempty" url:"uuid,omitempty"` + Base64 []byte `json:"base64,omitempty" url:"base64,omitempty"` + List []string `json:"list,omitempty" url:"list,omitempty"` + Set []string `json:"set,omitempty" url:"set,omitempty"` + Map map[int]string `json:"map,omitempty" url:"map,omitempty"` + Bigint *string `json:"bigint,omitempty" url:"bigint,omitempty"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (o *ObjectWithOptionalField) GetFieldString() *string { + if o == nil { + return nil + } + return o.FieldString +} + +func (o *ObjectWithOptionalField) GetInteger() *int { + if o == nil { + return nil + } + return o.Integer +} + +func (o *ObjectWithOptionalField) GetLong() *int64 { + if o == nil { + return nil + } + return o.Long +} + +func (o *ObjectWithOptionalField) GetDouble() *float64 { + if o == nil { + return nil + } + return o.Double +} + +func (o *ObjectWithOptionalField) GetBool() *bool { + if o == nil { + return nil + } + return o.Bool +} + +func (o *ObjectWithOptionalField) GetDatetime() *time.Time { + if o == nil { + return nil + } + return o.Datetime +} + +func (o *ObjectWithOptionalField) GetDate() *time.Time { + if o == nil { + return nil + } + return o.Date +} + +func (o *ObjectWithOptionalField) GetUuid() *uuid.UUID { + if o == nil { + return nil + } + return o.Uuid +} + +func (o *ObjectWithOptionalField) GetBase64() []byte { + if o == nil { + return nil + } + return o.Base64 +} + +func (o *ObjectWithOptionalField) GetList() []string { + if o == nil { + return nil + } + return o.List +} + +func (o *ObjectWithOptionalField) GetSet() []string { + if o == nil { + return nil + } + return o.Set +} + +func (o *ObjectWithOptionalField) GetMap() map[int]string { + if o == nil { + return nil + } + return o.Map +} + +func (o *ObjectWithOptionalField) GetBigint() *string { + if o == nil { + return nil + } + return o.Bigint +} + +func (o *ObjectWithOptionalField) GetExtraProperties() map[string]any { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithOptionalField) UnmarshalJSON( + data []byte, +) error { + type embed ObjectWithOptionalField + var unmarshaler = struct { + embed + Datetime *internal.DateTime `json:"datetime"` + Date *internal.Date `json:"date"` + }{ + embed: embed(*o), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *o = ObjectWithOptionalField(unmarshaler.embed) + o.Datetime = unmarshaler.Datetime.TimePtr() + o.Date = unmarshaler.Date.TimePtr() + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithOptionalField) MarshalJSON() ([]byte, error) { + type embed ObjectWithOptionalField + var marshaler = struct { + embed + Datetime *internal.DateTime `json:"datetime"` + Date *internal.Date `json:"date"` + }{ + embed: embed(*o), + Datetime: internal.NewOptionalDateTime(o.Datetime), + Date: internal.NewOptionalDate(o.Date), + } + return json.Marshal(marshaler) +} + +func (o *ObjectWithOptionalField) String() string { + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +type ObjectWithRequiredField struct { + FieldString string `json:"string" url:"string"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (o *ObjectWithRequiredField) GetFieldString() string { + if o == nil { + return "" + } + return o.FieldString +} + +func (o *ObjectWithRequiredField) GetExtraProperties() map[string]any { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithRequiredField) UnmarshalJSON( + data []byte, +) error { + type unmarshaler ObjectWithRequiredField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithRequiredField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithRequiredField) String() string { + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +type ObjectWithMapOfMap struct { + Map map[string]map[string]string `json:"map" url:"map"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (o *ObjectWithMapOfMap) GetMap() map[string]map[string]string { + if o == nil { + return nil + } + return o.Map +} + +func (o *ObjectWithMapOfMap) GetExtraProperties() map[string]any { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithMapOfMap) UnmarshalJSON( + data []byte, +) error { + type unmarshaler ObjectWithMapOfMap + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithMapOfMap(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithMapOfMap) String() string { + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +type NestedObjectWithOptionalField struct { + FieldString *string `json:"string,omitempty" url:"string,omitempty"` + NestedObject *ObjectWithOptionalField `json:"NestedObject,omitempty" url:"NestedObject,omitempty"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (n *NestedObjectWithOptionalField) GetFieldString() *string { + if n == nil { + return nil + } + return n.FieldString +} + +func (n *NestedObjectWithOptionalField) GetNestedObject() *ObjectWithOptionalField { + if n == nil { + return nil + } + return n.NestedObject +} + +func (n *NestedObjectWithOptionalField) GetExtraProperties() map[string]any { + if n == nil { + return nil + } + return n.extraProperties +} + +func (n *NestedObjectWithOptionalField) UnmarshalJSON( + data []byte, +) error { + type unmarshaler NestedObjectWithOptionalField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *n = NestedObjectWithOptionalField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *n) + if err != nil { + return err + } + n.extraProperties = extraProperties + n.rawJSON = json.RawMessage(data) + return nil +} + +func (n *NestedObjectWithOptionalField) String() string { + if len(n.rawJSON) > 0 { + if value, err := internal.StringifyJSON(n.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(n); err == nil { + return value + } + return fmt.Sprintf("%#v", n) +} + +type NestedObjectWithRequiredField struct { + FieldString string `json:"string" url:"string"` + NestedObject *ObjectWithOptionalField `json:"NestedObject" url:"NestedObject"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (n *NestedObjectWithRequiredField) GetFieldString() string { + if n == nil { + return "" + } + return n.FieldString +} + +func (n *NestedObjectWithRequiredField) GetNestedObject() *ObjectWithOptionalField { + if n == nil { + return nil + } + return n.NestedObject +} + +func (n *NestedObjectWithRequiredField) GetExtraProperties() map[string]any { + if n == nil { + return nil + } + return n.extraProperties +} + +func (n *NestedObjectWithRequiredField) UnmarshalJSON( + data []byte, +) error { + type unmarshaler NestedObjectWithRequiredField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *n = NestedObjectWithRequiredField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *n) + if err != nil { + return err + } + n.extraProperties = extraProperties + n.rawJSON = json.RawMessage(data) + return nil +} + +func (n *NestedObjectWithRequiredField) String() string { + if len(n.rawJSON) > 0 { + if value, err := internal.StringifyJSON(n.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(n); err == nil { + return value + } + return fmt.Sprintf("%#v", n) +} + +type DoubleOptional struct { + OptionalAlias *OptionalAlias `json:"optionalAlias,omitempty" url:"optionalAlias,omitempty"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (d *DoubleOptional) GetOptionalAlias() *OptionalAlias { + if d == nil { + return nil + } + return d.OptionalAlias +} + +func (d *DoubleOptional) GetExtraProperties() map[string]any { + if d == nil { + return nil + } + return d.extraProperties +} + +func (d *DoubleOptional) UnmarshalJSON( + data []byte, +) error { + type unmarshaler DoubleOptional + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *d = DoubleOptional(value) + extraProperties, err := internal.ExtractExtraProperties(data, *d) + if err != nil { + return err + } + d.extraProperties = extraProperties + d.rawJSON = json.RawMessage(data) + return nil +} + +func (d *DoubleOptional) String() string { + if len(d.rawJSON) > 0 { + if value, err := internal.StringifyJSON(d.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(d); err == nil { + return value + } + return fmt.Sprintf("%#v", d) +} + +type OptionalAlias = *string + +// This type tests that string fields containing datetime-like values +// are NOT reformatted by the wire test generator. The string field +// should preserve its exact value even if it looks like a datetime. +type ObjectWithDatetimeLikeString struct { + // A string field that happens to contain a datetime-like value + DatetimeLikeString string `json:"datetimeLikeString" url:"datetimeLikeString"` + // An actual datetime field for comparison + ActualDatetime time.Time `json:"actualDatetime" url:"actualDatetime"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (o *ObjectWithDatetimeLikeString) GetDatetimeLikeString() string { + if o == nil { + return "" + } + return o.DatetimeLikeString +} + +func (o *ObjectWithDatetimeLikeString) GetActualDatetime() time.Time { + if o == nil { + return time.Time{} + } + return o.ActualDatetime +} + +func (o *ObjectWithDatetimeLikeString) GetExtraProperties() map[string]any { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithDatetimeLikeString) UnmarshalJSON( + data []byte, +) error { + type embed ObjectWithDatetimeLikeString + var unmarshaler = struct { + embed + ActualDatetime *internal.DateTime `json:"actualDatetime"` + }{ + embed: embed(*o), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *o = ObjectWithDatetimeLikeString(unmarshaler.embed) + o.ActualDatetime = unmarshaler.ActualDatetime.Time() + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithDatetimeLikeString) MarshalJSON() ([]byte, error) { + type embed ObjectWithDatetimeLikeString + var marshaler = struct { + embed + ActualDatetime *internal.DateTime `json:"actualDatetime"` + }{ + embed: embed(*o), + ActualDatetime: internal.NewDateTime(o.ActualDatetime), + } + return json.Marshal(marshaler) +} + +func (o *ObjectWithDatetimeLikeString) String() string { + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} + +// Tests that unknown/any values containing backslashes in map keys +// are properly escaped in Go string literals. +type ObjectWithUnknownField struct { + Unknown any `json:"unknown" url:"unknown"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (o *ObjectWithUnknownField) GetUnknown() any { + if o == nil { + return nil + } + return o.Unknown +} + +func (o *ObjectWithUnknownField) GetExtraProperties() map[string]any { + if o == nil { + return nil + } + return o.extraProperties +} + +func (o *ObjectWithUnknownField) UnmarshalJSON( + data []byte, +) error { + type unmarshaler ObjectWithUnknownField + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *o = ObjectWithUnknownField(value) + extraProperties, err := internal.ExtractExtraProperties(data, *o) + if err != nil { + return err + } + o.extraProperties = extraProperties + o.rawJSON = json.RawMessage(data) + return nil +} + +func (o *ObjectWithUnknownField) String() string { + if len(o.rawJSON) > 0 { + if value, err := internal.StringifyJSON(o.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(o); err == nil { + return value + } + return fmt.Sprintf("%#v", o) +} diff --git a/seed/go-model/go-deterministic-ordering/types/union.go b/seed/go-model/go-deterministic-ordering/types/union.go new file mode 100644 index 000000000000..097cd14c3f83 --- /dev/null +++ b/seed/go-model/go-deterministic-ordering/types/union.go @@ -0,0 +1,140 @@ +// Code generated by Fern. DO NOT EDIT. + +package types + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/go-deterministic-ordering/fern/internal" +) + +type Animal struct { + Animal string + Dog Dog + Cat Cat +} + +type Dog struct { + Name string `json:"name" url:"name"` + LikesToWoof bool `json:"likesToWoof" url:"likesToWoof"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (d *Dog) GetName() string { + if d == nil { + return "" + } + return d.Name +} + +func (d *Dog) GetLikesToWoof() bool { + if d == nil { + return false + } + return d.LikesToWoof +} + +func (d *Dog) GetExtraProperties() map[string]any { + if d == nil { + return nil + } + return d.extraProperties +} + +func (d *Dog) UnmarshalJSON( + data []byte, +) error { + type unmarshaler Dog + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *d = Dog(value) + extraProperties, err := internal.ExtractExtraProperties(data, *d) + if err != nil { + return err + } + d.extraProperties = extraProperties + d.rawJSON = json.RawMessage(data) + return nil +} + +func (d *Dog) String() string { + if len(d.rawJSON) > 0 { + if value, err := internal.StringifyJSON(d.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(d); err == nil { + return value + } + return fmt.Sprintf("%#v", d) +} + +type Cat struct { + Name string `json:"name" url:"name"` + LikesToMeow bool `json:"likesToMeow" url:"likesToMeow"` + + extraProperties map[string]any + rawJSON json.RawMessage +} + +func (c *Cat) GetName() string { + if c == nil { + return "" + } + return c.Name +} + +func (c *Cat) GetLikesToMeow() bool { + if c == nil { + return false + } + return c.LikesToMeow +} + +func (c *Cat) GetExtraProperties() map[string]any { + if c == nil { + return nil + } + return c.extraProperties +} + +func (c *Cat) UnmarshalJSON( + data []byte, +) error { + type unmarshaler Cat + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *c = Cat(value) + extraProperties, err := internal.ExtractExtraProperties(data, *c) + if err != nil { + return err + } + c.extraProperties = extraProperties + c.rawJSON = json.RawMessage(data) + return nil +} + +func (c *Cat) String() string { + if len(c.rawJSON) > 0 { + if value, err := internal.StringifyJSON(c.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(c); err == nil { + return value + } + return fmt.Sprintf("%#v", c) +} + +type MixedType struct { + Double float64 + Boolean bool + String string + StringList []string +} From 480424cc6222fa39e0acb80bdd615a5006749be5 Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:08:37 -0400 Subject: [PATCH 18/29] chore(go): update go-sdk seed (#13536) Co-authored-by: patrickthornton --- .../dynamic-snippets/example58/snippet.go | 28 +++++++++++++++++++ .../dynamic-snippets/example58/snippet.go | 28 +++++++++++++++++++ .../.github/workflows/ci.yml | 4 +++ .../dynamic-snippets/example56/snippet.go | 13 ++------- .../dynamic-snippets/example58/snippet.go | 13 ++------- .../internal/query.go | 7 ++++- .../internal/query_test.go | 2 +- 7 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 seed/go-sdk/exhaustive/no-custom-config/dynamic-snippets/example58/snippet.go create mode 100644 seed/go-sdk/exhaustive/omit-empty-request-wrappers/dynamic-snippets/example58/snippet.go diff --git a/seed/go-sdk/exhaustive/no-custom-config/dynamic-snippets/example58/snippet.go b/seed/go-sdk/exhaustive/no-custom-config/dynamic-snippets/example58/snippet.go new file mode 100644 index 000000000000..1e71655744a0 --- /dev/null +++ b/seed/go-sdk/exhaustive/no-custom-config/dynamic-snippets/example58/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/exhaustive/fern/client" + option "github.com/exhaustive/fern/option" + fern "github.com/exhaustive/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.ReqWithHeaders{ + XTestServiceHeader: "X-TEST-SERVICE-HEADER", + XTestEndpointHeader: "X-TEST-ENDPOINT-HEADER", + Body: "string", + } + client.ReqWithHeaders.GetWithCustomHeader( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/exhaustive/omit-empty-request-wrappers/dynamic-snippets/example58/snippet.go b/seed/go-sdk/exhaustive/omit-empty-request-wrappers/dynamic-snippets/example58/snippet.go new file mode 100644 index 000000000000..1e71655744a0 --- /dev/null +++ b/seed/go-sdk/exhaustive/omit-empty-request-wrappers/dynamic-snippets/example58/snippet.go @@ -0,0 +1,28 @@ +package example + +import ( + client "github.com/exhaustive/fern/client" + option "github.com/exhaustive/fern/option" + fern "github.com/exhaustive/fern" + context "context" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithToken( + "", + ), + ) + request := &fern.ReqWithHeaders{ + XTestServiceHeader: "X-TEST-SERVICE-HEADER", + XTestEndpointHeader: "X-TEST-ENDPOINT-HEADER", + Body: "string", + } + client.ReqWithHeaders.GetWithCustomHeader( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml b/seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml index 588cbe574004..1097e6a18acc 100644 --- a/seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml +++ b/seed/go-sdk/go-deterministic-ordering/.github/workflows/ci.yml @@ -2,6 +2,10 @@ name: ci on: [push] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: compile: runs-on: ubuntu-latest diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example56/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example56/snippet.go index 1e71655744a0..f96bbce7845f 100644 --- a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example56/snippet.go +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example56/snippet.go @@ -1,9 +1,8 @@ package example import ( - client "github.com/exhaustive/fern/client" - option "github.com/exhaustive/fern/option" - fern "github.com/exhaustive/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" context "context" ) @@ -16,13 +15,7 @@ func do() { "", ), ) - request := &fern.ReqWithHeaders{ - XTestServiceHeader: "X-TEST-SERVICE-HEADER", - XTestEndpointHeader: "X-TEST-ENDPOINT-HEADER", - Body: "string", - } - client.ReqWithHeaders.GetWithCustomHeader( + client.Endpoints.Urls.WithMixedCase( context.TODO(), - request, ) } diff --git a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example58/snippet.go b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example58/snippet.go index 1e71655744a0..d147adf8d6bb 100644 --- a/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example58/snippet.go +++ b/seed/go-sdk/go-deterministic-ordering/dynamic-snippets/example58/snippet.go @@ -1,9 +1,8 @@ package example import ( - client "github.com/exhaustive/fern/client" - option "github.com/exhaustive/fern/option" - fern "github.com/exhaustive/fern" + client "github.com/go-deterministic-ordering/fern/client" + option "github.com/go-deterministic-ordering/fern/option" context "context" ) @@ -16,13 +15,7 @@ func do() { "", ), ) - request := &fern.ReqWithHeaders{ - XTestServiceHeader: "X-TEST-SERVICE-HEADER", - XTestEndpointHeader: "X-TEST-ENDPOINT-HEADER", - Body: "string", - } - client.ReqWithHeaders.GetWithCustomHeader( + client.Endpoints.Urls.WithEndingSlash( context.TODO(), - request, ) } diff --git a/seed/go-sdk/go-deterministic-ordering/internal/query.go b/seed/go-sdk/go-deterministic-ordering/internal/query.go index 1cbaf7fe1c02..9b567f7a5563 100644 --- a/seed/go-sdk/go-deterministic-ordering/internal/query.go +++ b/seed/go-sdk/go-deterministic-ordering/internal/query.go @@ -11,6 +11,11 @@ import ( "github.com/google/uuid" ) +// RFC3339Milli is a time format string for RFC 3339 with millisecond precision. +// Go's time.RFC3339 omits fractional seconds and time.RFC3339Nano trims trailing +// zeros, so neither produces the fixed ".000" millisecond suffix that many APIs expect. +const RFC3339Milli = "2006-01-02T15:04:05.000Z07:00" + var ( bytesType = reflect.TypeOf([]byte{}) queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem() @@ -277,7 +282,7 @@ func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) strin if format := sf.Tag.Get("format"); format == "date" { return t.Format("2006-01-02") } - return t.Format(time.RFC3339) + return t.Format(RFC3339Milli) } if v.Type() == uuidType { diff --git a/seed/go-sdk/go-deterministic-ordering/internal/query_test.go b/seed/go-sdk/go-deterministic-ordering/internal/query_test.go index 2c28cb8acf68..5b463e297350 100644 --- a/seed/go-sdk/go-deterministic-ordering/internal/query_test.go +++ b/seed/go-sdk/go-deterministic-ordering/internal/query_test.go @@ -115,7 +115,7 @@ func TestQueryValues(t *testing.T) { }, ) require.NoError(t, err) - assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode()) + assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56.000Z", values.Encode()) }) t.Run("date", func(t *testing.T) { From 1c77f88a1ae4abc152d837b91983db2154949f0e Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:19:09 -0400 Subject: [PATCH 19/29] chore(go): update go-sdk seed (#13537) Co-authored-by: fern-support From 8316061de2a07aff0e7a692656e47465a395d264 Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:20:43 -0400 Subject: [PATCH 20/29] chore(go): update go-model seed (#13538) Co-authored-by: fern-support From dc57ed75e18cdf3e06d8d7d2370ceef9ce84bde3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:21:44 +0000 Subject: [PATCH 21/29] chore(deps): update yauzl to 3.2.1 to address CVE-2026-31988 (#13533) * [Dependabot Alert #877] Scaffold PR for yauzl * fix: update yauzl to 3.2.1 to address CVE-2026-31988 Co-Authored-By: unknown <> * fix: minimize lockfile diff to only yauzl 3.2.0 -> 3.2.1 changes Co-Authored-By: unknown <> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 12 ++++++------ pnpm-workspace.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f350fdaf43f6..d376b77d804b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -598,8 +598,8 @@ catalogs: specifier: ^0.6.2 version: 0.6.2 yauzl: - specifier: ^3.2.0 - version: 3.2.0 + specifier: ^3.2.1 + version: 3.2.1 yazl: specifier: ^3.3.1 version: 3.3.1 @@ -6592,7 +6592,7 @@ importers: version: 4.0.1 yauzl: specifier: 'catalog:' - version: 3.2.0 + version: 3.2.1 devDependencies: '@fern-api/configs': specifier: workspace:* @@ -14813,8 +14813,8 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yauzl@3.2.0: - resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==} + yauzl@3.2.1: + resolution: {integrity: sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==} engines: {node: '>=12'} yazl@3.3.1: @@ -22213,7 +22213,7 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - yauzl@3.2.0: + yauzl@3.2.1: dependencies: buffer-crc32: 0.2.13 pend: 1.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5657dc19cc4e..ef965193e9fd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -221,6 +221,6 @@ catalog: ws: ^8.17.1 xml2js: ^0.6.2 yaml: ^2.4.5 - yauzl: ^3.2.0 + yauzl: ^3.2.1 yazl: ^3.3.1 zod: ^3.22.3 From 432047f51a722c6f59697c6eee8554abb03f0b76 Mon Sep 17 00:00:00 2001 From: David Konigsberg <72822263+davidkonigsberg@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:33:06 -0400 Subject: [PATCH 22/29] fix(ci): use squash merge for dependabot auto-merge (#13541) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 44be0336ff24..680eadb668c2 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -25,7 +25,7 @@ jobs: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs if: ${{contains(steps.metadata.outputs.update-type, 'version-update')}} - run: gh pr merge --auto --merge "$PR_URL" + run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} From 787a40e89506e3e98a9526a5c645fc6ab12f0265 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:36:45 -0400 Subject: [PATCH 23/29] chore(deps): add pnpm override for yauzl ^3.2.1 to resolve Dependabot alert #877 (#13542) Co-Authored-By: unknown <> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- package.json | 3 ++- pnpm-lock.yaml | 25 ++++--------------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 6120f455cf34..646b0c826ecb 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,8 @@ "ts-essentials": "^10.1.1", "form-data": "^4.0.4", "@fern-api/ui-core-utils": "0.145.12-b50d999d1", - "vite": "^7.0.0" + "vite": "^7.0.0", + "yauzl": "^3.2.1" } }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d376b77d804b..feea2b3aa43f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -597,9 +597,6 @@ catalogs: xml2js: specifier: ^0.6.2 version: 0.6.2 - yauzl: - specifier: ^3.2.1 - version: 3.2.1 yazl: specifier: ^3.3.1 version: 3.3.1 @@ -627,6 +624,7 @@ overrides: form-data: ^4.0.4 '@fern-api/ui-core-utils': 0.145.12-b50d999d1 vite: ^7.0.0 + yauzl: ^3.2.1 importers: @@ -6591,7 +6589,7 @@ importers: specifier: ^4.0.1 version: 4.0.1 yauzl: - specifier: 'catalog:' + specifier: ^3.2.1 version: 3.2.1 devDependencies: '@fern-api/configs': @@ -11931,9 +11929,6 @@ packages: fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -14810,9 +14805,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yauzl@3.2.1: resolution: {integrity: sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==} engines: {node: '>=12'} @@ -18190,7 +18182,7 @@ snapshots: file-type: 3.9.0 get-stream: 2.3.1 pify: 2.3.0 - yauzl: 2.10.0 + yauzl: 3.2.1 decompress@4.2.1: dependencies: @@ -18639,7 +18631,7 @@ snapshots: dependencies: debug: 4.4.3 get-stream: 5.2.0 - yauzl: 2.10.0 + yauzl: 3.2.1 optionalDependencies: '@types/yauzl': 2.10.3 transitivePeerDependencies: @@ -18687,10 +18679,6 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -22208,11 +22196,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - yauzl@3.2.1: dependencies: buffer-crc32: 0.2.13 From 388afdcae743614294d94e8a0b5a4a71a2580f0b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:40:01 -0400 Subject: [PATCH 24/29] chore(deps): update flatted to >=3.4.0 to fix CVE-2026-32141 (#13534) * [Dependabot Alert #876] Scaffold PR for flatted * fix(deps): update flatted to >=3.4.0 to fix CVE-2026-32141 Co-Authored-By: unknown <> * fix(deps): re-resolve lockfile to update flatted 3.3.3 -> 3.4.1 (no override needed) Co-Authored-By: unknown <> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index feea2b3aa43f..5fe2712447f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12003,8 +12003,8 @@ packages: flat-cache@6.1.20: resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatted@3.4.1: + resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} @@ -18113,7 +18113,7 @@ snapshots: cspell-io: 9.7.0 cspell-lib: 9.7.0 fast-json-stable-stringify: 2.1.0 - flatted: 3.3.3 + flatted: 3.4.1 semver: 7.7.4 tinyglobby: 0.2.15 @@ -18753,22 +18753,22 @@ snapshots: flat-cache@3.2.0: dependencies: - flatted: 3.3.3 + flatted: 3.4.1 keyv: 4.5.4 rimraf: 3.0.2 flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.1 keyv: 4.5.4 flat-cache@6.1.20: dependencies: cacheable: 2.3.2 - flatted: 3.3.3 + flatted: 3.4.1 hookified: 1.15.0 - flatted@3.3.3: {} + flatted@3.4.1: {} follow-redirects@1.15.11: {} From 48f2c8ce97c9371c46de95293f46c2d5fa531845 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:40:59 +0000 Subject: [PATCH 25/29] chore(deps): bump undici from 6.23.0 to 6.24.0 (#13540) Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.0) --- updated-dependencies: - dependency-name: undici dependency-version: 6.24.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 12 ++++++------ pnpm-workspace.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fe2712447f6..5a08fe7db589 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -568,8 +568,8 @@ catalogs: specifier: ^1.0.37 version: 1.0.41 undici: - specifier: ^6.23.0 - version: 6.23.0 + specifier: ^6.24.0 + version: 6.24.0 unified: specifier: ^11.0.5 version: 11.0.5 @@ -4908,7 +4908,7 @@ importers: version: 5.9.3 undici: specifier: 'catalog:' - version: 6.23.0 + version: 6.24.0 url-join: specifier: ^4.0.1 version: 4.0.1 @@ -14458,8 +14458,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@6.23.0: - resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} + undici@6.24.0: + resolution: {integrity: sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==} engines: {node: '>=18.17'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -21816,7 +21816,7 @@ snapshots: undici-types@7.16.0: {} - undici@6.23.0: {} + undici@6.24.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ef965193e9fd..6aded45696e6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -208,7 +208,7 @@ catalog: turbo: ^2.8.10 typescript: 5.9.3 ua-parser-js: ^1.0.37 - undici: ^6.23.0 + undici: ^6.24.0 unified: ^11.0.5 unist-util-visit: ^5.0.0 url-join: ^4.0.1 From 3c1bab70c5483da708fe13f36262e5eaf4eee239 Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:47:08 -0400 Subject: [PATCH 26/29] chore(csharp): update csharp-sdk seed (#13532) Co-authored-by: davidkonigsberg From 4b8e7f541d6a4bd28fd865253d81da409fa87ff6 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:00:41 -0400 Subject: [PATCH 27/29] feat(typescript): add passthrough fetch method to generated SDK clients (#12920) Co-authored-by: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/GeneratedSdkClientClassImpl.ts | 66 +++ generators/typescript/sdk/features.yml | 7 + .../ReadmeSnippetBuilder.test.ts.snap | 10 + .../src/readme/ReadmeSnippetBuilder.ts | 22 + generators/typescript/sdk/versions.yml | 16 + .../core-utilities/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 401 +++++++++++++++++ seed/ts-sdk/accept-header/README.md | 21 + seed/ts-sdk/accept-header/src/Client.ts | 33 ++ .../accept-header/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/alias-extends/README.md | 21 + seed/ts-sdk/alias-extends/src/Client.ts | 31 ++ .../alias-extends/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/alias/README.md | 21 + seed/ts-sdk/alias/src/Client.ts | 31 ++ seed/ts-sdk/alias/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../generate-endpoint-metadata/README.md | 21 + .../generate-endpoint-metadata/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../any-auth/no-custom-config/README.md | 21 + .../any-auth/no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/api-wide-base-path/README.md | 21 + seed/ts-sdk/api-wide-base-path/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../audiences/no-custom-config/README.md | 21 + .../audiences/no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../audiences/with-partner-audience/README.md | 21 + .../with-partner-audience/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/basic-auth/README.md | 21 + seed/ts-sdk/basic-auth/src/Client.ts | 33 ++ .../basic-auth/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/bytes-download/README.md | 21 + seed/ts-sdk/bytes-download/src/Client.ts | 32 ++ .../bytes-download/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/bytes-upload/README.md | 21 + seed/ts-sdk/bytes-upload/src/Client.ts | 32 ++ .../bytes-upload/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/client-side-params/README.md | 21 + seed/ts-sdk/client-side-params/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/content-type/README.md | 21 + seed/ts-sdk/content-type/src/Client.ts | 32 ++ .../content-type/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../serde-layer/README.md | 21 + .../serde-layer/src/Client.ts | 32 ++ .../serde-layer/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../empty-clients/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/endpoint-security-auth/README.md | 21 + .../endpoint-security-auth/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../enum/forward-compatible-enums/README.md | 21 + .../forward-compatible-enums/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/enum/no-custom-config/README.md | 21 + .../enum/no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/enum/serde/README.md | 21 + seed/ts-sdk/enum/serde/src/Client.ts | 32 ++ .../enum/serde/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../error-property/no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../error-property/union-utils/README.md | 21 + .../error-property/union-utils/src/Client.ts | 32 ++ .../union-utils/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/errors/README.md | 21 + seed/ts-sdk/errors/src/Client.ts | 32 ++ seed/ts-sdk/errors/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../examples-with-api-reference/README.md | 21 + .../examples-with-api-reference/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../examples/retain-original-casing/README.md | 21 + .../retain-original-casing/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../exhaustive/allow-extra-fields/README.md | 21 + .../allow-extra-fields/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../exhaustive/bigint-serde-layer/README.md | 21 + .../bigint-serde-layer/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ seed/ts-sdk/exhaustive/bigint/README.md | 21 + seed/ts-sdk/exhaustive/bigint/src/Client.ts | 33 ++ .../bigint/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ .../consolidate-type-files/README.md | 21 + .../consolidate-type-files/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../export-all-requests-at-root/README.md | 21 + .../export-all-requests-at-root/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../local-files-no-source/cjs/Client.d.ts | 12 + .../local-files-no-source/cjs/Client.js | 67 +++ .../cjs/core/fetcher/index.d.ts | 2 + .../cjs/core/fetcher/index.js | 4 +- .../core/fetcher/makePassthroughRequest.d.ts | 49 +++ .../core/fetcher/makePassthroughRequest.js | 135 ++++++ .../local-files-no-source/esm/Client.d.mts | 12 + .../local-files-no-source/esm/Client.mjs | 34 ++ .../esm/core/fetcher/index.d.mts | 2 + .../esm/core/fetcher/index.mjs | 1 + .../core/fetcher/makePassthroughRequest.d.mts | 49 +++ .../core/fetcher/makePassthroughRequest.mjs | 132 ++++++ seed/ts-sdk/exhaustive/local-files/Client.ts | 33 ++ .../local-files/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../exhaustive/multiple-exports/README.md | 21 + .../exhaustive/multiple-exports/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../exhaustive/never-throw-errors/README.md | 21 + .../never-throw-errors/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../exhaustive/no-custom-config/README.md | 21 + .../exhaustive/no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/exhaustive/node-fetch/README.md | 21 + .../exhaustive/node-fetch/src/Client.ts | 33 ++ .../node-fetch/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../exhaustive/output-src-only/Client.ts | 33 ++ .../output-src-only/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ seed/ts-sdk/exhaustive/package-path/README.md | 21 + .../src/test-packagePath/Client.ts | 33 ++ .../test-packagePath/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-camel-case/README.md | 21 + .../parameter-naming-camel-case/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-original-name/README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-snake-case/README.md | 21 + .../parameter-naming-snake-case/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-wire-value/README.md | 21 + .../parameter-naming-wire-value/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../retain-original-casing/README.md | 21 + .../retain-original-casing/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/exhaustive/serde-layer/README.md | 21 + .../exhaustive/serde-layer/src/Client.ts | 33 ++ .../serde-layer/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/exhaustive/use-jest/README.md | 21 + seed/ts-sdk/exhaustive/use-jest/src/Client.ts | 33 ++ .../use-jest/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ .../exhaustive/web-stream-wrapper/README.md | 21 + .../web-stream-wrapper/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../with-audiences/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/extends/README.md | 21 + seed/ts-sdk/extends/src/Client.ts | 31 ++ seed/ts-sdk/extends/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/extra-properties/README.md | 21 + seed/ts-sdk/extra-properties/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../file-download-response-headers/README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../file-download/no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../file-download/stream-wrapper/README.md | 21 + .../stream-wrapper/src/Client.ts | 32 ++ .../stream-wrapper/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ seed/ts-sdk/file-upload-openapi/README.md | 21 + seed/ts-sdk/file-upload-openapi/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../file-upload/form-data-node16/README.md | 21 + .../form-data-node16/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/file-upload/inline/README.md | 21 + seed/ts-sdk/file-upload/inline/src/Client.ts | 32 ++ .../inline/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../file-upload/no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/file-upload/serde/README.md | 21 + seed/ts-sdk/file-upload/serde/src/Client.ts | 32 ++ .../serde/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/file-upload/use-jest/README.md | 21 + .../ts-sdk/file-upload/use-jest/src/Client.ts | 32 ++ .../use-jest/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ seed/ts-sdk/file-upload/wrapper/README.md | 21 + seed/ts-sdk/file-upload/wrapper/src/Client.ts | 32 ++ .../wrapper/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ seed/ts-sdk/folders/README.md | 21 + seed/ts-sdk/folders/src/Client.ts | 31 ++ seed/ts-sdk/folders/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/header-auth/README.md | 21 + seed/ts-sdk/header-auth/src/Client.ts | 33 ++ .../header-auth/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/http-head/README.md | 21 + seed/ts-sdk/http-head/src/Client.ts | 32 ++ .../http-head/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/idempotency-headers/README.md | 21 + seed/ts-sdk/idempotency-headers/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../imdb/branded-string-aliases/README.md | 21 + .../imdb/branded-string-aliases/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/imdb/no-custom-config/README.md | 21 + .../imdb/no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/imdb/omit-undefined/README.md | 21 + seed/ts-sdk/imdb/omit-undefined/src/Client.ts | 32 ++ .../omit-undefined/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/inferred-auth-explicit/README.md | 21 + .../inferred-auth-explicit/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../inferred-auth-implicit-api-key/README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/inferred-auth-implicit/README.md | 21 + .../inferred-auth-implicit/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/license/README.md | 21 + seed/ts-sdk/license/src/Client.ts | 31 ++ seed/ts-sdk/license/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/literal/README.md | 21 + seed/ts-sdk/literal/src/Client.ts | 32 ++ seed/ts-sdk/literal/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../literals-unions/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../mixed-case/no-custom-config/README.md | 21 + .../mixed-case/no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../retain-original-casing/README.md | 21 + .../retain-original-casing/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/mixed-file-directory/README.md | 21 + .../ts-sdk/mixed-file-directory/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/multi-line-docs/README.md | 21 + seed/ts-sdk/multi-line-docs/src/Client.ts | 32 ++ .../multi-line-docs/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/multi-url-environment/README.md | 21 + .../multi-url-environment/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/multiple-request-bodies/README.md | 21 + .../multiple-request-bodies/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/no-environment/README.md | 21 + seed/ts-sdk/no-environment/src/Client.ts | 33 ++ .../no-environment/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/no-retries/README.md | 21 + seed/ts-sdk/no-retries/src/Client.ts | 32 ++ .../no-retries/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/nullable-allof-extends/README.md | 21 + .../nullable-allof-extends/src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/nullable-optional/README.md | 21 + seed/ts-sdk/nullable-optional/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/nullable-request-body/README.md | 21 + .../nullable-request-body/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/nullable/README.md | 21 + seed/ts-sdk/nullable/src/Client.ts | 32 ++ .../ts-sdk/nullable/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../oauth-client-credentials-custom/README.md | 21 + .../package.json | 5 +- .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../never-throw-errors/README.md | 21 + .../never-throw-errors/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 165 +++++++ .../fetcher/makePassthroughRequest.test.ts | 350 +++++++++++++++ .../oauth-client-credentials/serde/README.md | 21 + .../serde/src/Client.ts | 33 ++ .../serde/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/object/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/optional/README.md | 21 + seed/ts-sdk/optional/src/Client.ts | 32 ++ .../ts-sdk/optional/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/package-yml/README.md | 21 + seed/ts-sdk/package-yml/src/Client.ts | 31 ++ .../package-yml/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/pagination-custom/README.md | 21 + seed/ts-sdk/pagination-custom/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/pagination-uri-path/README.md | 21 + seed/ts-sdk/pagination-uri-path/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../pagination/no-custom-config/README.md | 21 + .../pagination/no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../pagination/page-index-semantics/README.md | 21 + .../page-index-semantics/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-inline-path-parameters-serde/README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-inline-path-parameters/README.md | 21 + .../no-inline-path-parameters/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-camel-case/README.md | 21 + .../parameter-naming-camel-case/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-original-name/README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-snake-case/README.md | 21 + .../parameter-naming-snake-case/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-wire-value/README.md | 21 + .../parameter-naming-wire-value/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../retain-original-casing/README.md | 21 + .../retain-original-casing/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/plain-text/README.md | 21 + seed/ts-sdk/plain-text/src/Client.ts | 32 ++ .../plain-text/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../generate-read-write-only-types/README.md | 21 + .../src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/public-object/src/Client.ts | 32 ++ .../public-object/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../ts-sdk/query-parameters-openapi/README.md | 21 + .../query-parameters-openapi/src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-camel-case/README.md | 21 + .../parameter-naming-camel-case/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-original-name/README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-snake-case/README.md | 21 + .../parameter-naming-snake-case/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../parameter-naming-wire-value/README.md | 21 + .../parameter-naming-wire-value/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/query-parameters/serde/README.md | 21 + .../query-parameters/serde/src/Client.ts | 32 ++ .../serde/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../flatten-request-parameters/README.md | 21 + .../flatten-request-parameters/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/required-nullable/README.md | 21 + seed/ts-sdk/required-nullable/src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/reserved-keywords/README.md | 21 + seed/ts-sdk/reserved-keywords/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/response-property/README.md | 21 + seed/ts-sdk/response-property/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../server-sent-event-examples/README.md | 21 + .../server-sent-event-examples/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/server-sent-events/README.md | 21 + seed/ts-sdk/server-sent-events/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/server-url-templating/README.md | 21 + .../server-url-templating/src/Client.ts | 30 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../simple-api/allow-custom-fetcher/README.md | 21 + .../allow-custom-fetcher/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../simple-api/allow-extra-fields/README.md | 21 + .../allow-extra-fields/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-api/bundle/README.md | 21 + seed/ts-sdk/simple-api/bundle/src/Client.ts | 33 ++ .../bundle/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../simple-api/custom-package-json/README.md | 21 + .../custom-package-json/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-api/jsr/README.md | 21 + seed/ts-sdk/simple-api/jsr/src/Client.ts | 33 ++ .../simple-api/jsr/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../simple-api/legacy-exports/README.md | 21 + .../simple-api/legacy-exports/src/Client.ts | 33 ++ .../legacy-exports/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../simple-api/no-custom-config/README.md | 21 + .../simple-api/no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-linter-and-formatter/README.md | 21 + .../no-linter-and-formatter/src/Client.ts | 24 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 402 ++++++++++++++++++ seed/ts-sdk/simple-api/no-scripts/README.md | 21 + .../simple-api/no-scripts/src/Client.ts | 24 ++ .../no-scripts/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 402 ++++++++++++++++++ seed/ts-sdk/simple-api/oidc-token/README.md | 21 + .../simple-api/oidc-token/src/Client.ts | 33 ++ .../oidc-token/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../simple-api/omit-fern-headers/README.md | 21 + .../omit-fern-headers/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-api/use-oxc/README.md | 25 ++ seed/ts-sdk/simple-api/use-oxc/src/Client.ts | 32 ++ .../use-oxc/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-api/use-oxfmt/README.md | 25 ++ .../ts-sdk/simple-api/use-oxfmt/src/Client.ts | 33 ++ .../use-oxfmt/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-api/use-oxlint/README.md | 21 + .../simple-api/use-oxlint/src/Client.ts | 32 ++ .../use-oxlint/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../use-prettier-no-linter/README.md | 25 ++ .../use-prettier-no-linter/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-api/use-prettier/README.md | 25 ++ .../simple-api/use-prettier/src/Client.ts | 33 ++ .../use-prettier/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-api/use-yarn/README.md | 21 + seed/ts-sdk/simple-api/use-yarn/src/Client.ts | 33 ++ .../use-yarn/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/simple-fhir/README.md | 21 + seed/ts-sdk/simple-fhir/src/Client.ts | 31 ++ .../simple-fhir/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../single-url-environment-default/README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/streaming-parameter/README.md | 21 + seed/ts-sdk/streaming-parameter/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../streaming/no-custom-config/README.md | 21 + .../streaming/no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/streaming/serde-layer/README.md | 21 + .../streaming/serde-layer/src/Client.ts | 32 ++ .../serde-layer/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/streaming/wrapper/README.md | 21 + seed/ts-sdk/streaming/wrapper/src/Client.ts | 32 ++ .../wrapper/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 397 +++++++++++++++++ seed/ts-sdk/trace/exhaustive/README.md | 21 + seed/ts-sdk/trace/exhaustive/src/Client.ts | 33 ++ .../exhaustive/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/trace/no-custom-config/README.md | 21 + .../trace/no-custom-config/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/trace/serde-no-throwing/README.md | 21 + .../trace/serde-no-throwing/src/Client.ts | 33 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/trace/serde/README.md | 21 + seed/ts-sdk/trace/serde/src/Client.ts | 33 ++ .../trace/serde/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/ts-express-casing/README.md | 21 + seed/ts-sdk/ts-express-casing/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/ts-extra-properties/README.md | 21 + seed/ts-sdk/ts-extra-properties/src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/ts-inline-types/inline/README.md | 21 + .../ts-inline-types/inline/src/Client.ts | 31 ++ .../inline/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../ts-inline-types/no-inline/README.md | 21 + .../ts-inline-types/no-inline/src/Client.ts | 31 ++ .../no-inline/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../README.md | 21 + .../src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-custom-config/README.md | 21 + .../no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../skip-response-validation/README.md | 21 + .../skip-response-validation/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/unions-with-local-date/README.md | 21 + .../unions-with-local-date/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/unions/README.md | 21 + seed/ts-sdk/unions/src/Client.ts | 32 ++ seed/ts-sdk/unions/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../ts-sdk/unknown/no-custom-config/README.md | 21 + .../unknown/no-custom-config/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/unknown/unknown-as-any/README.md | 21 + .../unknown/unknown-as-any/src/Client.ts | 32 ++ .../unknown-as-any/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/url-form-encoded/README.md | 21 + seed/ts-sdk/url-form-encoded/src/Client.ts | 31 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/validation/README.md | 21 + seed/ts-sdk/validation/src/Client.ts | 31 ++ .../validation/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/variables/README.md | 21 + seed/ts-sdk/variables/src/Client.ts | 32 ++ .../variables/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/version-no-default/README.md | 21 + seed/ts-sdk/version-no-default/src/Client.ts | 32 ++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ seed/ts-sdk/version/README.md | 21 + seed/ts-sdk/version/src/Client.ts | 32 ++ seed/ts-sdk/version/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../websockets/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../websockets/README.md | 21 + .../websockets/src/Client.ts | 33 ++ .../websockets/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../no-serde/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ .../websocket/serde/src/core/fetcher/index.ts | 2 + .../core/fetcher/makePassthroughRequest.ts | 189 ++++++++ .../fetcher/makePassthroughRequest.test.ts | 398 +++++++++++++++++ 948 files changed, 122848 insertions(+), 5 deletions(-) create mode 100644 generators/typescript/utils/core-utilities/src/core/fetcher/makePassthroughRequest.ts create mode 100644 generators/typescript/utils/core-utilities/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/accept-header/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/accept-header/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/alias-extends/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/alias-extends/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/alias/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/alias/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/any-auth/generate-endpoint-metadata/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/api-wide-base-path/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/api-wide-base-path/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/audiences/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/audiences/with-partner-audience/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/basic-auth/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/bytes-download/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/bytes-download/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/bytes-upload/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/bytes-upload/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/circular-references/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/client-side-params/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/client-side-params/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/content-type/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/content-type/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/cross-package-type-names/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/cross-package-type-names/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/dollar-string-examples/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/dollar-string-examples/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/empty-clients/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/empty-clients/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/endpoint-security-auth/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/endpoint-security-auth/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/enum/forward-compatible-enums-with-serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/enum/forward-compatible-enums/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/enum/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/enum/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/enum/serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/enum/serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/error-property/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/errors/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/errors/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/bigint-serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/bigint/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/bigint/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/consolidate-type-files/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/export-all-requests-at-root/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.d.ts create mode 100644 seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.js create mode 100644 seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.d.mts create mode 100644 seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.mjs create mode 100644 seed/ts-sdk/exhaustive/local-files/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/multiple-exports/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/node-fetch/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/output-src-only/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/package-path/src/test-packagePath/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/web-stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/exhaustive/with-audiences/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/extends/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/extends/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/extra-properties/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-download/file-download-response-headers/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-download/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-download/stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-upload-openapi/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-upload-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-upload/form-data-node16/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-upload/inline/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-upload/inline/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-upload/serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-upload/serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-upload/use-jest/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-upload/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/file-upload/wrapper/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/file-upload/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/folders/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/folders/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/header-auth-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/header-auth/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/header-auth/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/http-head/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/http-head/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/imdb/branded-string-aliases/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/imdb/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/imdb/omit-undefined/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/inferred-auth-explicit/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/inferred-auth-implicit-api-key/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/inferred-auth-implicit-no-expiry/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/inferred-auth-implicit/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/license/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/license/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/literal/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/literal/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/literals-unions/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/literals-unions/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/mixed-file-directory/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/multiple-request-bodies/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/multiple-request-bodies/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/no-environment/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/no-retries/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/no-retries/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/nullable-allof-extends/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/nullable-allof-extends/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/nullable-optional/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/nullable-optional/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/nullable-request-body/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/nullable-request-body/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/nullable/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/nullable/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-custom/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-reference/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-with-variables/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/object/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/object/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/optional/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/optional/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/package-yml/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/pagination-custom/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/pagination-custom/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/pagination-uri-path/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/pagination-uri-path/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/pagination/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/pagination/page-index-semantics/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/no-inline-path-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/path-parameters/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/plain-text/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/plain-text/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/property-access/generate-read-write-only-types/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/property-access/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/public-object/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/public-object/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters-openapi-as-objects/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters-openapi/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/query-parameters/serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/query-parameters/serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/request-parameters/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/request-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/request-parameters/use-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/required-nullable/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/required-nullable/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/response-property/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/server-sent-event-examples/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/server-sent-event-examples/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/server-sent-events/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/server-url-templating/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/server-url-templating/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/allow-custom-fetcher/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/bundle/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/bundle/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/custom-package-json/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/jsr/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/jsr/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/legacy-exports/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/no-linter-and-formatter/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/no-scripts/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/oidc-token/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/omit-fern-headers/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/use-oxc/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/use-oxfmt/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/use-oxlint/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/use-prettier-no-linter/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/use-prettier/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-api/use-yarn/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/simple-fhir/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/simple-fhir/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/streaming-parameter/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/streaming-parameter/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/streaming/serde-layer/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/streaming/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/streaming/wrapper/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/streaming/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/trace/serde-no-throwing/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/trace/serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/trace/serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/ts-express-casing/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/ts-express-casing/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/ts-extra-properties/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/ts-extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/ts-inline-types/inline/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/ts-inline-types/no-inline/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/undiscriminated-union-with-response-property/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/unions-with-local-date/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/unions-with-local-date/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/unions/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/unions/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/url-form-encoded/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/url-form-encoded/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/validation/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/validation/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/variables/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/variables/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/version-no-default/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/version-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/version/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/version/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/webhook-audience/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/webhook-audience/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/webhooks/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/websocket-bearer-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/websocket-inferred-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/websocket/no-serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/websocket/no-serde/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/websocket/no-websocket-clients/tests/unit/fetcher/makePassthroughRequest.test.ts create mode 100644 seed/ts-sdk/websocket/serde/src/core/fetcher/makePassthroughRequest.ts create mode 100644 seed/ts-sdk/websocket/serde/tests/unit/fetcher/makePassthroughRequest.test.ts diff --git a/generators/typescript/sdk/client-class-generator/src/GeneratedSdkClientClassImpl.ts b/generators/typescript/sdk/client-class-generator/src/GeneratedSdkClientClassImpl.ts index 8c87a77ce29f..ff90ad9070b4 100644 --- a/generators/typescript/sdk/client-class-generator/src/GeneratedSdkClientClassImpl.ts +++ b/generators/typescript/sdk/client-class-generator/src/GeneratedSdkClientClassImpl.ts @@ -827,6 +827,11 @@ export class GeneratedSdkClientClassImpl implements GeneratedSdkClientClass { serviceModule.statements.push(this.generateIdempotentRequestOptionsInterface(context)); } + // Add passthrough fetch method on root client + if (this.isRoot) { + this.addPassthroughFetchMethod({ serviceClass, context }); + } + for (const wrappedService of this.generatedWrappedServices) { wrappedService.addToServiceClass({ isRoot: this.isRoot, @@ -855,6 +860,67 @@ export class GeneratedSdkClientClassImpl implements GeneratedSdkClientClass { return code`this._options = normalizeClientOptionsWithAuth(options);`; } + private addPassthroughFetchMethod({ + serviceClass, + context + }: { + serviceClass: SetRequired; + context: SdkContext; + }): void { + // Build the auth headers getter expression + const hasAuth = this.authProvider && this.anyEndpointWithAuth; + let getAuthHeadersCode: string; + if (hasAuth) { + getAuthHeadersCode = + "getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers,"; + } else { + getAuthHeadersCode = ""; + } + + // For multi-URL environments, this._options.environment is an object (e.g. { ec2: string; s3: string }), + // not a single string URL. Only pass `environment` when it resolves to a single base URL string. + const isMultiUrlEnvironment = + this.intermediateRepresentation.environments?.environments.type === "multipleBaseUrls"; + const environmentCode = isMultiUrlEnvironment ? "" : "environment: this._options.environment,"; + + const fetchMethodBody = ` +return core.makePassthroughRequest(input, init, { + ${environmentCode} + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + ${getAuthHeadersCode} +}, requestOptions);`; + + const fetchMethod: MethodDeclarationStructure = { + kind: StructureKind.Method, + scope: Scope.Public, + isAsync: true, + name: "fetch", + docs: [ + "Make a passthrough request using the SDK's configured auth, retry, logging, etc.\n" + + "This is useful for making requests to endpoints not yet supported in the SDK.\n" + + "The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL.\n\n" + + "@param {Request | string | URL} input - The URL, path, or Request object.\n" + + "@param {RequestInit} init - Standard fetch RequestInit options.\n" + + "@param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal).\n" + + "@returns {Promise} A standard Response object." + ], + parameters: [ + { name: "input", type: "Request | string | URL" }, + { name: "init", type: "RequestInit", hasQuestionToken: true }, + { name: "requestOptions", type: "core.PassthroughRequest.RequestOptions", hasQuestionToken: true } + ], + returnType: "Promise", + statements: fetchMethodBody + }; + + serviceClass.methods.push(fetchMethod); + } + public getBaseUrl(endpoint: FernIr.HttpEndpoint, context: SdkContext): ts.Expression { const referenceToBaseUrl = this.getReferenceToBaseUrl(context); diff --git a/generators/typescript/sdk/features.yml b/generators/typescript/sdk/features.yml index ed4398826280..102cf06da885 100644 --- a/generators/typescript/sdk/features.yml +++ b/generators/typescript/sdk/features.yml @@ -159,6 +159,13 @@ features: ``` + - id: CUSTOM_FETCH + advanced: true + description: | + The SDK provides a low-level `fetch` method for making custom HTTP requests while still + benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. + This is useful for calling API endpoints not yet supported in the SDK. + - id: RUNTIME_COMPATIBILITY advanced: true description: | diff --git a/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap b/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap index df76c6530784..d5417c4adebd 100644 --- a/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap +++ b/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap @@ -44,6 +44,16 @@ const response = await client.createUser(..., { ], "AUTHENTICATION": false, "BINARY_RESPONSE": [], + "CUSTOM_FETCH": [ + "const response = await client.fetch("/v1/custom/endpoint", { method: "GET" }, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { "X-Custom-Header": "custom-value" }, +}); + +const data = await response.json(); +", + ], "EXCEPTION_HANDLING": [ "import { MySDKError } from "@acme/sdk"; diff --git a/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts b/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts index 6220e48044d3..95377663a26a 100644 --- a/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts +++ b/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts @@ -37,6 +37,7 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder { public static readonly FILE_UPLOAD_REQUEST_FEATURE_ID: FernGeneratorCli.FeatureId = "FILE_UPLOADS"; public static readonly STREAMING_RESPONSE_FEATURE_ID: FernGeneratorCli.FeatureId = "STREAMING_RESPONSE"; public static readonly LOGGING_FEATURE_ID: FernGeneratorCli.FeatureId = "LOGGING"; + private static readonly CUSTOM_FETCH_FEATURE_ID: FernGeneratorCli.FeatureId = "CUSTOM_FETCH"; private readonly context: SdkContext; private readonly isPaginationEnabled: boolean; @@ -97,6 +98,7 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder { snippets[ReadmeSnippetBuilder.ADDITIONAL_QUERY_STRING_PARAMETERS_FEATURE_ID] = this.buildAdditionalQueryStringParametersSnippets(); snippets[ReadmeSnippetBuilder.LOGGING_FEATURE_ID] = this.buildLoggingSnippets(); + snippets[ReadmeSnippetBuilder.CUSTOM_FETCH_FEATURE_ID] = this.buildCustomFetchSnippets(); if (this.isPaginationEnabled) { const paginationSnippets = this.buildPaginationSnippets(); @@ -483,6 +485,26 @@ const ${this.clientVariableName} = new ${this.rootClientConstructorName}({ ]; } + private buildCustomFetchSnippets(): string[] { + return [ + this.writeCode( + code` +const response = await ${this.clientVariableName}.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +` + ) + ]; + } + private buildAuthenticationSnippets(): string[] | false { // Return false to explicitly skip snippets - the full description is built in buildAuthenticationDescription() return false; diff --git a/generators/typescript/sdk/versions.yml b/generators/typescript/sdk/versions.yml index b10f2183ab62..110c2edb2c18 100644 --- a/generators/typescript/sdk/versions.yml +++ b/generators/typescript/sdk/versions.yml @@ -1,4 +1,19 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.55.0 + changelogEntry: + - summary: | + Add a passthrough `fetch` method to the root client class of all generated TypeScript SDKs. + The method mimics the standard `fetch` API while automatically applying the SDK's configured + authentication, base URL resolution, retry logic with exponential backoff, request timeouts, + custom headers, and logging. This allows users to make requests to endpoints not yet exposed + in the SDK definition (e.g., new public endpoints, closed beta endpoints) without additional + setup. The method accepts three parameters: `url` (string or relative path), `init` (standard + `RequestInit`), and `requestOptions` (SDK-specific overrides for timeout, retries, headers, + and abort signal). + type: feat + createdAt: "2026-03-13" + irVersion: 65 + - version: 3.54.0 changelogEntry: - summary: | @@ -290,6 +305,7 @@ type: feat createdAt: "2026-02-27" irVersion: 65 + - version: 3.52.0 changelogEntry: - summary: | diff --git a/generators/typescript/utils/core-utilities/src/core/fetcher/index.ts b/generators/typescript/utils/core-utilities/src/core/fetcher/index.ts index 2f32091ef658..6e165a0de2cf 100644 --- a/generators/typescript/utils/core-utilities/src/core/fetcher/index.ts +++ b/generators/typescript/utils/core-utilities/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher"; export { fetcher } from "./Fetcher"; export { getHeader } from "./getHeader"; export { HttpResponsePromise } from "./HttpResponsePromise"; +export type { PassthroughRequest } from "./makePassthroughRequest"; +export { makePassthroughRequest } from "./makePassthroughRequest"; export type { RawResponse, WithRawResponse } from "./RawResponse"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse"; export { Supplier } from "./Supplier"; diff --git a/generators/typescript/utils/core-utilities/src/core/fetcher/makePassthroughRequest.ts b/generators/typescript/utils/core-utilities/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..82166dcf114c --- /dev/null +++ b/generators/typescript/utils/core-utilities/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger"; +import { join } from "../url/join"; +import { EndpointSupplier } from "./EndpointSupplier"; +import { getFetchFn } from "./getFetchFn"; +import { makeRequest } from "./makeRequest"; +import { requestWithRetries } from "./requestWithRetries"; +import { Supplier } from "./Supplier"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/generators/typescript/utils/core-utilities/tests/unit/fetcher/makePassthroughRequest.test.ts b/generators/typescript/utils/core-utilities/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..4e7fbc05f7f1 --- /dev/null +++ b/generators/typescript/utils/core-utilities/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,401 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers["authorization"]).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest( + "https://api.example.com", + { headers: initHeaders }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "include" }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "same-origin" }, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/accept-header/README.md b/seed/ts-sdk/accept-header/README.md index 2697a4b37611..59f5405285b2 100644 --- a/seed/ts-sdk/accept-header/README.md +++ b/seed/ts-sdk/accept-header/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/accept-header/src/Client.ts b/seed/ts-sdk/accept-header/src/Client.ts index 582d09d1b42f..3d8be67f372e 100644 --- a/seed/ts-sdk/accept-header/src/Client.ts +++ b/seed/ts-sdk/accept-header/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedAcceptClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedAcceptClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/accept-header/src/core/fetcher/index.ts b/seed/ts-sdk/accept-header/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/accept-header/src/core/fetcher/index.ts +++ b/seed/ts-sdk/accept-header/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/accept-header/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/accept-header/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/accept-header/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/accept-header/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/accept-header/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/accept-header/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/alias-extends/README.md b/seed/ts-sdk/alias-extends/README.md index 1cd9a3584479..159656cc7c7e 100644 --- a/seed/ts-sdk/alias-extends/README.md +++ b/seed/ts-sdk/alias-extends/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -232,6 +233,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/alias-extends/src/Client.ts b/seed/ts-sdk/alias-extends/src/Client.ts index 3c728b32d39f..96f15b86c1c7 100644 --- a/seed/ts-sdk/alias-extends/src/Client.ts +++ b/seed/ts-sdk/alias-extends/src/Client.ts @@ -80,4 +80,35 @@ export class SeedAliasExtendsClient { "/extends/extended-inline-request-body", ); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/alias-extends/src/core/fetcher/index.ts b/seed/ts-sdk/alias-extends/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/alias-extends/src/core/fetcher/index.ts +++ b/seed/ts-sdk/alias-extends/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/alias-extends/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/alias-extends/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/alias-extends/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/alias-extends/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/alias-extends/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/alias-extends/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/alias/README.md b/seed/ts-sdk/alias/README.md index 24152a92e9e2..227401640523 100644 --- a/seed/ts-sdk/alias/README.md +++ b/seed/ts-sdk/alias/README.md @@ -19,6 +19,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -215,6 +216,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/alias/src/Client.ts b/seed/ts-sdk/alias/src/Client.ts index 419c44d0e35c..249d65f286e1 100644 --- a/seed/ts-sdk/alias/src/Client.ts +++ b/seed/ts-sdk/alias/src/Client.ts @@ -69,4 +69,35 @@ export class SeedAliasClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/{typeId}"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/alias/src/core/fetcher/index.ts b/seed/ts-sdk/alias/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/alias/src/core/fetcher/index.ts +++ b/seed/ts-sdk/alias/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/alias/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/alias/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/alias/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/alias/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/alias/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/any-auth/generate-endpoint-metadata/README.md b/seed/ts-sdk/any-auth/generate-endpoint-metadata/README.md index a68a2aa45e41..d191c1fd69ec 100644 --- a/seed/ts-sdk/any-auth/generate-endpoint-metadata/README.md +++ b/seed/ts-sdk/any-auth/generate-endpoint-metadata/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -275,6 +276,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/Client.ts b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/Client.ts index 45c9039c9580..8977ec5a2a35 100644 --- a/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/Client.ts +++ b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/Client.ts @@ -4,6 +4,7 @@ import { AuthClient } from "./api/resources/auth/client/Client.js"; import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedAnyAuthClient { export type Options = BaseClientOptions; @@ -27,4 +28,36 @@ export class SeedAnyAuthClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/index.ts b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/index.ts +++ b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/any-auth/generate-endpoint-metadata/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/any-auth/generate-endpoint-metadata/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/any-auth/generate-endpoint-metadata/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/any-auth/generate-endpoint-metadata/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/any-auth/no-custom-config/README.md b/seed/ts-sdk/any-auth/no-custom-config/README.md index a68a2aa45e41..d191c1fd69ec 100644 --- a/seed/ts-sdk/any-auth/no-custom-config/README.md +++ b/seed/ts-sdk/any-auth/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -275,6 +276,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/any-auth/no-custom-config/src/Client.ts b/seed/ts-sdk/any-auth/no-custom-config/src/Client.ts index 45c9039c9580..8977ec5a2a35 100644 --- a/seed/ts-sdk/any-auth/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/any-auth/no-custom-config/src/Client.ts @@ -4,6 +4,7 @@ import { AuthClient } from "./api/resources/auth/client/Client.js"; import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedAnyAuthClient { export type Options = BaseClientOptions; @@ -27,4 +28,36 @@ export class SeedAnyAuthClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/any-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/any-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/api-wide-base-path/README.md b/seed/ts-sdk/api-wide-base-path/README.md index 3c24e79fdba4..ba47a330b1a0 100644 --- a/seed/ts-sdk/api-wide-base-path/README.md +++ b/seed/ts-sdk/api-wide-base-path/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/api-wide-base-path/src/Client.ts b/seed/ts-sdk/api-wide-base-path/src/Client.ts index 39d921147727..1bbc72c1df6f 100644 --- a/seed/ts-sdk/api-wide-base-path/src/Client.ts +++ b/seed/ts-sdk/api-wide-base-path/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedApiWideBasePathClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedApiWideBasePathClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/api-wide-base-path/src/core/fetcher/index.ts b/seed/ts-sdk/api-wide-base-path/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/api-wide-base-path/src/core/fetcher/index.ts +++ b/seed/ts-sdk/api-wide-base-path/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/api-wide-base-path/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/api-wide-base-path/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/api-wide-base-path/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/api-wide-base-path/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/api-wide-base-path/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/api-wide-base-path/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/audiences/no-custom-config/README.md b/seed/ts-sdk/audiences/no-custom-config/README.md index 63aac21877c9..8eba131a10f9 100644 --- a/seed/ts-sdk/audiences/no-custom-config/README.md +++ b/seed/ts-sdk/audiences/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -244,6 +245,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/audiences/no-custom-config/src/Client.ts b/seed/ts-sdk/audiences/no-custom-config/src/Client.ts index 18260a06dfab..8e7a4768c77c 100644 --- a/seed/ts-sdk/audiences/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/audiences/no-custom-config/src/Client.ts @@ -5,6 +5,7 @@ import { FolderDClient } from "./api/resources/folderD/client/Client.js"; import { FooClient } from "./api/resources/foo/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedAudiencesClient { export type Options = BaseClientOptions; @@ -33,4 +34,35 @@ export class SeedAudiencesClient { public get foo(): FooClient { return (this._foo ??= new FooClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/audiences/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/audiences/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/audiences/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/audiences/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/audiences/with-partner-audience/README.md b/seed/ts-sdk/audiences/with-partner-audience/README.md index 21ae4d72f3e8..fd3ed1f11cf4 100644 --- a/seed/ts-sdk/audiences/with-partner-audience/README.md +++ b/seed/ts-sdk/audiences/with-partner-audience/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/audiences/with-partner-audience/src/Client.ts b/seed/ts-sdk/audiences/with-partner-audience/src/Client.ts index 9971e99021ee..9993659dcef4 100644 --- a/seed/ts-sdk/audiences/with-partner-audience/src/Client.ts +++ b/seed/ts-sdk/audiences/with-partner-audience/src/Client.ts @@ -3,6 +3,7 @@ import { FolderDClient } from "./api/resources/folderD/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedAudiencesClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedAudiencesClient { public get folderD(): FolderDClient { return (this._folderD ??= new FolderDClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/index.ts b/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/index.ts +++ b/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/audiences/with-partner-audience/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/audiences/with-partner-audience/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/audiences/with-partner-audience/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/audiences/with-partner-audience/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/README.md b/seed/ts-sdk/basic-auth-environment-variables/README.md index e0a24afba920..8a2de1571fe9 100644 --- a/seed/ts-sdk/basic-auth-environment-variables/README.md +++ b/seed/ts-sdk/basic-auth-environment-variables/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -228,6 +229,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/basic-auth-environment-variables/src/Client.ts b/seed/ts-sdk/basic-auth-environment-variables/src/Client.ts index eb9aad8d407d..d68219f101ba 100644 --- a/seed/ts-sdk/basic-auth-environment-variables/src/Client.ts +++ b/seed/ts-sdk/basic-auth-environment-variables/src/Client.ts @@ -3,6 +3,7 @@ import { BasicAuthClient } from "./api/resources/basicAuth/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedBasicAuthEnvironmentVariablesClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedBasicAuthEnvironmentVariablesClient { public get basicAuth(): BasicAuthClient { return (this._basicAuth ??= new BasicAuthClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/index.ts b/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/index.ts +++ b/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth/README.md b/seed/ts-sdk/basic-auth/README.md index f22b5cf1250e..0e2f96b57414 100644 --- a/seed/ts-sdk/basic-auth/README.md +++ b/seed/ts-sdk/basic-auth/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -228,6 +229,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/basic-auth/src/Client.ts b/seed/ts-sdk/basic-auth/src/Client.ts index b32546c6a70c..f6718a70786d 100644 --- a/seed/ts-sdk/basic-auth/src/Client.ts +++ b/seed/ts-sdk/basic-auth/src/Client.ts @@ -3,6 +3,7 @@ import { BasicAuthClient } from "./api/resources/basicAuth/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedBasicAuthClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedBasicAuthClient { public get basicAuth(): BasicAuthClient { return (this._basicAuth ??= new BasicAuthClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/basic-auth/src/core/fetcher/index.ts b/seed/ts-sdk/basic-auth/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/basic-auth/src/core/fetcher/index.ts +++ b/seed/ts-sdk/basic-auth/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/basic-auth/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/basic-auth/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/basic-auth/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/basic-auth/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/basic-auth/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/README.md b/seed/ts-sdk/bearer-token-environment-variable/README.md index 8ae3dd3cd9c6..9398cbcf6aac 100644 --- a/seed/ts-sdk/bearer-token-environment-variable/README.md +++ b/seed/ts-sdk/bearer-token-environment-variable/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/bearer-token-environment-variable/src/Client.ts b/seed/ts-sdk/bearer-token-environment-variable/src/Client.ts index 01b36258c1b1..00a64162073a 100644 --- a/seed/ts-sdk/bearer-token-environment-variable/src/Client.ts +++ b/seed/ts-sdk/bearer-token-environment-variable/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedBearerTokenEnvironmentVariableClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedBearerTokenEnvironmentVariableClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/index.ts b/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/index.ts +++ b/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/bytes-download/README.md b/seed/ts-sdk/bytes-download/README.md index f553abb908c2..48bfdfcbc892 100644 --- a/seed/ts-sdk/bytes-download/README.md +++ b/seed/ts-sdk/bytes-download/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -616,6 +617,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/bytes-download/src/Client.ts b/seed/ts-sdk/bytes-download/src/Client.ts index 159a2dc099df..6474498931b6 100644 --- a/seed/ts-sdk/bytes-download/src/Client.ts +++ b/seed/ts-sdk/bytes-download/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedBytesDownloadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedBytesDownloadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/bytes-download/src/core/fetcher/index.ts b/seed/ts-sdk/bytes-download/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/bytes-download/src/core/fetcher/index.ts +++ b/seed/ts-sdk/bytes-download/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/bytes-download/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/bytes-download/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/bytes-download/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/bytes-download/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/bytes-download/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/bytes-download/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/bytes-upload/README.md b/seed/ts-sdk/bytes-upload/README.md index 8ef43ce0ae27..ad53c1a746d1 100644 --- a/seed/ts-sdk/bytes-upload/README.md +++ b/seed/ts-sdk/bytes-upload/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -273,6 +274,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/bytes-upload/src/Client.ts b/seed/ts-sdk/bytes-upload/src/Client.ts index 8f952ac1bab4..38dd903facbe 100644 --- a/seed/ts-sdk/bytes-upload/src/Client.ts +++ b/seed/ts-sdk/bytes-upload/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedBytesUploadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedBytesUploadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/bytes-upload/src/core/fetcher/index.ts b/seed/ts-sdk/bytes-upload/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/bytes-upload/src/core/fetcher/index.ts +++ b/seed/ts-sdk/bytes-upload/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/bytes-upload/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/bytes-upload/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/bytes-upload/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/bytes-upload/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/bytes-upload/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/bytes-upload/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references-advanced/src/core/fetcher/index.ts b/seed/ts-sdk/circular-references-advanced/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/circular-references-advanced/src/core/fetcher/index.ts +++ b/seed/ts-sdk/circular-references-advanced/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/circular-references-advanced/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/circular-references-advanced/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references/src/core/fetcher/index.ts b/seed/ts-sdk/circular-references/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/circular-references/src/core/fetcher/index.ts +++ b/seed/ts-sdk/circular-references/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/circular-references/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/circular-references/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/circular-references/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/circular-references/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/circular-references/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/client-side-params/README.md b/seed/ts-sdk/client-side-params/README.md index 4e5fe560100d..da10164b3be5 100644 --- a/seed/ts-sdk/client-side-params/README.md +++ b/seed/ts-sdk/client-side-params/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -249,6 +250,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/client-side-params/src/Client.ts b/seed/ts-sdk/client-side-params/src/Client.ts index 8f131cb543e0..8302b6233ba8 100644 --- a/seed/ts-sdk/client-side-params/src/Client.ts +++ b/seed/ts-sdk/client-side-params/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedClientSideParamsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedClientSideParamsClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/client-side-params/src/core/fetcher/index.ts b/seed/ts-sdk/client-side-params/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/client-side-params/src/core/fetcher/index.ts +++ b/seed/ts-sdk/client-side-params/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/client-side-params/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/client-side-params/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/client-side-params/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/client-side-params/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/client-side-params/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/client-side-params/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/content-type/README.md b/seed/ts-sdk/content-type/README.md index 117bec2b7a96..caf8c37aaf90 100644 --- a/seed/ts-sdk/content-type/README.md +++ b/seed/ts-sdk/content-type/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/content-type/src/Client.ts b/seed/ts-sdk/content-type/src/Client.ts index 29c79b74e1ad..ab163e2df8eb 100644 --- a/seed/ts-sdk/content-type/src/Client.ts +++ b/seed/ts-sdk/content-type/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedContentTypesClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedContentTypesClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/content-type/src/core/fetcher/index.ts b/seed/ts-sdk/content-type/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/content-type/src/core/fetcher/index.ts +++ b/seed/ts-sdk/content-type/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/content-type/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/content-type/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/content-type/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/content-type/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/content-type/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/content-type/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/cross-package-type-names/no-custom-config/README.md b/seed/ts-sdk/cross-package-type-names/no-custom-config/README.md index 28f0db245830..847e78994972 100644 --- a/seed/ts-sdk/cross-package-type-names/no-custom-config/README.md +++ b/seed/ts-sdk/cross-package-type-names/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -244,6 +245,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/cross-package-type-names/no-custom-config/src/Client.ts b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/Client.ts index 7037e8d9dcc4..120f67764496 100644 --- a/seed/ts-sdk/cross-package-type-names/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/Client.ts @@ -5,6 +5,7 @@ import { FolderDClient } from "./api/resources/folderD/client/Client.js"; import { FooClient } from "./api/resources/foo/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedCrossPackageTypeNamesClient { export type Options = BaseClientOptions; @@ -33,4 +34,35 @@ export class SeedCrossPackageTypeNamesClient { public get foo(): FooClient { return (this._foo ??= new FooClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/cross-package-type-names/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/cross-package-type-names/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/cross-package-type-names/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/cross-package-type-names/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/cross-package-type-names/serde-layer/README.md b/seed/ts-sdk/cross-package-type-names/serde-layer/README.md index 28f0db245830..847e78994972 100644 --- a/seed/ts-sdk/cross-package-type-names/serde-layer/README.md +++ b/seed/ts-sdk/cross-package-type-names/serde-layer/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -244,6 +245,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/cross-package-type-names/serde-layer/src/Client.ts b/seed/ts-sdk/cross-package-type-names/serde-layer/src/Client.ts index 7037e8d9dcc4..120f67764496 100644 --- a/seed/ts-sdk/cross-package-type-names/serde-layer/src/Client.ts +++ b/seed/ts-sdk/cross-package-type-names/serde-layer/src/Client.ts @@ -5,6 +5,7 @@ import { FolderDClient } from "./api/resources/folderD/client/Client.js"; import { FooClient } from "./api/resources/foo/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedCrossPackageTypeNamesClient { export type Options = BaseClientOptions; @@ -33,4 +34,35 @@ export class SeedCrossPackageTypeNamesClient { public get foo(): FooClient { return (this._foo ??= new FooClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/index.ts b/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/index.ts +++ b/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/cross-package-type-names/serde-layer/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/cross-package-type-names/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/cross-package-type-names/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/cross-package-type-names/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/dollar-string-examples/src/core/fetcher/index.ts b/seed/ts-sdk/dollar-string-examples/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/dollar-string-examples/src/core/fetcher/index.ts +++ b/seed/ts-sdk/dollar-string-examples/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/dollar-string-examples/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/dollar-string-examples/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/dollar-string-examples/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/dollar-string-examples/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/dollar-string-examples/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/dollar-string-examples/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/empty-clients/src/core/fetcher/index.ts b/seed/ts-sdk/empty-clients/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/empty-clients/src/core/fetcher/index.ts +++ b/seed/ts-sdk/empty-clients/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/empty-clients/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/empty-clients/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/empty-clients/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/empty-clients/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/empty-clients/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/empty-clients/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/endpoint-security-auth/README.md b/seed/ts-sdk/endpoint-security-auth/README.md index 51bfaade4efe..289902df4614 100644 --- a/seed/ts-sdk/endpoint-security-auth/README.md +++ b/seed/ts-sdk/endpoint-security-auth/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -275,6 +276,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/endpoint-security-auth/src/Client.ts b/seed/ts-sdk/endpoint-security-auth/src/Client.ts index b29e4f6576b3..cde2e1850334 100644 --- a/seed/ts-sdk/endpoint-security-auth/src/Client.ts +++ b/seed/ts-sdk/endpoint-security-auth/src/Client.ts @@ -4,6 +4,7 @@ import { AuthClient } from "./api/resources/auth/client/Client.js"; import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedEndpointSecurityAuthClient { export type Options = BaseClientOptions; @@ -27,4 +28,36 @@ export class SeedEndpointSecurityAuthClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/index.ts b/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/index.ts +++ b/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/endpoint-security-auth/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/endpoint-security-auth/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/endpoint-security-auth/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/endpoint-security-auth/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/README.md b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/README.md index 8e521b9be464..d685daf6704f 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/README.md +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/Client.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/Client.ts index 22bc54ac1dba..35f253fe01c7 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/Client.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/Client.ts @@ -7,6 +7,7 @@ import { PathParamClient } from "./api/resources/pathParam/client/Client.js"; import { QueryParamClient } from "./api/resources/queryParam/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedEnumClient { export type Options = BaseClientOptions; @@ -45,4 +46,35 @@ export class SeedEnumClient { public get queryParam(): QueryParamClient { return (this._queryParam ??= new QueryParamClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/index.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/enum/forward-compatible-enums/README.md b/seed/ts-sdk/enum/forward-compatible-enums/README.md index 8e521b9be464..d685daf6704f 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums/README.md +++ b/seed/ts-sdk/enum/forward-compatible-enums/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/enum/forward-compatible-enums/src/Client.ts b/seed/ts-sdk/enum/forward-compatible-enums/src/Client.ts index 22bc54ac1dba..35f253fe01c7 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums/src/Client.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums/src/Client.ts @@ -7,6 +7,7 @@ import { PathParamClient } from "./api/resources/pathParam/client/Client.js"; import { QueryParamClient } from "./api/resources/queryParam/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedEnumClient { export type Options = BaseClientOptions; @@ -45,4 +46,35 @@ export class SeedEnumClient { public get queryParam(): QueryParamClient { return (this._queryParam ??= new QueryParamClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/index.ts b/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/index.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/enum/forward-compatible-enums/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/enum/forward-compatible-enums/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/enum/forward-compatible-enums/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/enum/forward-compatible-enums/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/enum/no-custom-config/README.md b/seed/ts-sdk/enum/no-custom-config/README.md index 8e521b9be464..d685daf6704f 100644 --- a/seed/ts-sdk/enum/no-custom-config/README.md +++ b/seed/ts-sdk/enum/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/enum/no-custom-config/src/Client.ts b/seed/ts-sdk/enum/no-custom-config/src/Client.ts index 22bc54ac1dba..35f253fe01c7 100644 --- a/seed/ts-sdk/enum/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/enum/no-custom-config/src/Client.ts @@ -7,6 +7,7 @@ import { PathParamClient } from "./api/resources/pathParam/client/Client.js"; import { QueryParamClient } from "./api/resources/queryParam/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedEnumClient { export type Options = BaseClientOptions; @@ -45,4 +46,35 @@ export class SeedEnumClient { public get queryParam(): QueryParamClient { return (this._queryParam ??= new QueryParamClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/enum/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/enum/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/enum/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/enum/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/enum/serde/README.md b/seed/ts-sdk/enum/serde/README.md index 8e521b9be464..d685daf6704f 100644 --- a/seed/ts-sdk/enum/serde/README.md +++ b/seed/ts-sdk/enum/serde/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/enum/serde/src/Client.ts b/seed/ts-sdk/enum/serde/src/Client.ts index 22bc54ac1dba..35f253fe01c7 100644 --- a/seed/ts-sdk/enum/serde/src/Client.ts +++ b/seed/ts-sdk/enum/serde/src/Client.ts @@ -7,6 +7,7 @@ import { PathParamClient } from "./api/resources/pathParam/client/Client.js"; import { QueryParamClient } from "./api/resources/queryParam/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedEnumClient { export type Options = BaseClientOptions; @@ -45,4 +46,35 @@ export class SeedEnumClient { public get queryParam(): QueryParamClient { return (this._queryParam ??= new QueryParamClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/enum/serde/src/core/fetcher/index.ts b/seed/ts-sdk/enum/serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/enum/serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/enum/serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/enum/serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/enum/serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/enum/serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/enum/serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/enum/serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/enum/serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/error-property/no-custom-config/README.md b/seed/ts-sdk/error-property/no-custom-config/README.md index aa0210958732..a9c35831ba30 100644 --- a/seed/ts-sdk/error-property/no-custom-config/README.md +++ b/seed/ts-sdk/error-property/no-custom-config/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/error-property/no-custom-config/src/Client.ts b/seed/ts-sdk/error-property/no-custom-config/src/Client.ts index 6d500eea5a36..b88192a70325 100644 --- a/seed/ts-sdk/error-property/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/error-property/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { PropertyBasedErrorClient } from "./api/resources/propertyBasedError/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedErrorPropertyClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedErrorPropertyClient { public get propertyBasedError(): PropertyBasedErrorClient { return (this._propertyBasedError ??= new PropertyBasedErrorClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/error-property/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/error-property/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/error-property/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/error-property/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/error-property/union-utils/README.md b/seed/ts-sdk/error-property/union-utils/README.md index aa0210958732..a9c35831ba30 100644 --- a/seed/ts-sdk/error-property/union-utils/README.md +++ b/seed/ts-sdk/error-property/union-utils/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/error-property/union-utils/src/Client.ts b/seed/ts-sdk/error-property/union-utils/src/Client.ts index 6d500eea5a36..b88192a70325 100644 --- a/seed/ts-sdk/error-property/union-utils/src/Client.ts +++ b/seed/ts-sdk/error-property/union-utils/src/Client.ts @@ -3,6 +3,7 @@ import { PropertyBasedErrorClient } from "./api/resources/propertyBasedError/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedErrorPropertyClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedErrorPropertyClient { public get propertyBasedError(): PropertyBasedErrorClient { return (this._propertyBasedError ??= new PropertyBasedErrorClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/error-property/union-utils/src/core/fetcher/index.ts b/seed/ts-sdk/error-property/union-utils/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/error-property/union-utils/src/core/fetcher/index.ts +++ b/seed/ts-sdk/error-property/union-utils/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/error-property/union-utils/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/error-property/union-utils/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/errors/README.md b/seed/ts-sdk/errors/README.md index 6fd03579234d..37b20983186f 100644 --- a/seed/ts-sdk/errors/README.md +++ b/seed/ts-sdk/errors/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -228,6 +229,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/errors/src/Client.ts b/seed/ts-sdk/errors/src/Client.ts index 529e035e85bc..ecf0952b6d9f 100644 --- a/seed/ts-sdk/errors/src/Client.ts +++ b/seed/ts-sdk/errors/src/Client.ts @@ -3,6 +3,7 @@ import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedErrorsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedErrorsClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/errors/src/core/fetcher/index.ts b/seed/ts-sdk/errors/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/errors/src/core/fetcher/index.ts +++ b/seed/ts-sdk/errors/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/errors/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/errors/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/errors/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/errors/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/errors/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/errors/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/README.md b/seed/ts-sdk/examples/examples-with-api-reference/README.md index 2e0978f13477..3dae87146109 100644 --- a/seed/ts-sdk/examples/examples-with-api-reference/README.md +++ b/seed/ts-sdk/examples/examples-with-api-reference/README.md @@ -27,6 +27,7 @@ The CustomName TypeScript library provides convenient access to the CustomName A - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) ## Documentation @@ -288,6 +289,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/examples/examples-with-api-reference/src/Client.ts b/seed/ts-sdk/examples/examples-with-api-reference/src/Client.ts index 0b642ea177cf..fd6a9058aa50 100644 --- a/seed/ts-sdk/examples/examples-with-api-reference/src/Client.ts +++ b/seed/ts-sdk/examples/examples-with-api-reference/src/Client.ts @@ -135,4 +135,36 @@ export class SeedExamplesClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/index.ts b/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/index.ts +++ b/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/README.md b/seed/ts-sdk/examples/retain-original-casing/README.md index bfce111b836e..f0608d9cc9cf 100644 --- a/seed/ts-sdk/examples/retain-original-casing/README.md +++ b/seed/ts-sdk/examples/retain-original-casing/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -240,6 +241,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/examples/retain-original-casing/src/Client.ts b/seed/ts-sdk/examples/retain-original-casing/src/Client.ts index 0b642ea177cf..fd6a9058aa50 100644 --- a/seed/ts-sdk/examples/retain-original-casing/src/Client.ts +++ b/seed/ts-sdk/examples/retain-original-casing/src/Client.ts @@ -135,4 +135,36 @@ export class SeedExamplesClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/index.ts b/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/index.ts +++ b/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/README.md b/seed/ts-sdk/exhaustive/allow-extra-fields/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/allow-extra-fields/README.md +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/src/Client.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/allow-extra-fields/src/Client.ts +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/README.md b/seed/ts-sdk/exhaustive/bigint-serde-layer/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/README.md +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/Client.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/Client.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/bigint/README.md b/seed/ts-sdk/exhaustive/bigint/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/bigint/README.md +++ b/seed/ts-sdk/exhaustive/bigint/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/bigint/src/Client.ts b/seed/ts-sdk/exhaustive/bigint/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/bigint/src/Client.ts +++ b/seed/ts-sdk/exhaustive/bigint/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bigint/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/bigint/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/bigint/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bigint/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/consolidate-type-files/README.md b/seed/ts-sdk/exhaustive/consolidate-type-files/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/consolidate-type-files/README.md +++ b/seed/ts-sdk/exhaustive/consolidate-type-files/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/consolidate-type-files/src/Client.ts b/seed/ts-sdk/exhaustive/consolidate-type-files/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/consolidate-type-files/src/Client.ts +++ b/seed/ts-sdk/exhaustive/consolidate-type-files/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/consolidate-type-files/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/consolidate-type-files/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/consolidate-type-files/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/consolidate-type-files/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/export-all-requests-at-root/README.md b/seed/ts-sdk/exhaustive/export-all-requests-at-root/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/export-all-requests-at-root/README.md +++ b/seed/ts-sdk/exhaustive/export-all-requests-at-root/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/Client.ts b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/Client.ts +++ b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/export-all-requests-at-root/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/export-all-requests-at-root/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/export-all-requests-at-root/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/export-all-requests-at-root/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.d.ts b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.d.ts index e143e4822be4..5b01298b7820 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.d.ts +++ b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.d.ts @@ -5,6 +5,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { type Options = BaseClientOptions; interface RequestOptions extends BaseRequestOptions { @@ -23,4 +24,15 @@ export declare class SeedExhaustiveClient { get noAuth(): NoAuthClient; get noReqBody(): NoReqBodyClient; get reqWithHeaders(): ReqWithHeadersClient; + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + fetch(input: Request | string | URL, init?: RequestInit, requestOptions?: core.PassthroughRequest.RequestOptions): Promise; } diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.js b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.js index 6b746048a604..a9513c199071 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.js +++ b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/Client.js @@ -1,5 +1,47 @@ "use strict"; // This file was auto-generated by Fern from our API Definition. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.SeedExhaustiveClient = void 0; const Client_js_1 = require("./api/resources/endpoints/client/Client.js"); @@ -8,6 +50,7 @@ const Client_js_3 = require("./api/resources/noAuth/client/Client.js"); const Client_js_4 = require("./api/resources/noReqBody/client/Client.js"); const Client_js_5 = require("./api/resources/reqWithHeaders/client/Client.js"); const BaseClient_js_1 = require("./BaseClient.js"); +const core = __importStar(require("./core/index.js")); class SeedExhaustiveClient { constructor(options) { this._options = (0, BaseClient_js_1.normalizeClientOptionsWithAuth)(options); @@ -32,5 +75,29 @@ class SeedExhaustiveClient { var _a; return ((_a = this._reqWithHeaders) !== null && _a !== void 0 ? _a : (this._reqWithHeaders = new Client_js_5.ReqWithHeadersClient(this._options))); } + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + fetch(input, init, requestOptions) { + return __awaiter(this, void 0, void 0, function* () { + return core.makePassthroughRequest(input, init, { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: () => __awaiter(this, void 0, void 0, function* () { return (yield this._options.authProvider.getAuthRequest()).headers; }), + }, requestOptions); + }); + } } exports.SeedExhaustiveClient = SeedExhaustiveClient; diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.d.ts b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.d.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.d.ts +++ b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.d.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.js b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.js index 2cc33863c7c4..4a29fabdbe31 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.js +++ b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/index.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Supplier = exports.unknownRawResponse = exports.toRawResponse = exports.abortRawResponse = exports.HttpResponsePromise = exports.getHeader = exports.fetcher = exports.EndpointSupplier = void 0; +exports.Supplier = exports.unknownRawResponse = exports.toRawResponse = exports.abortRawResponse = exports.makePassthroughRequest = exports.HttpResponsePromise = exports.getHeader = exports.fetcher = exports.EndpointSupplier = void 0; var EndpointSupplier_js_1 = require("./EndpointSupplier.js"); Object.defineProperty(exports, "EndpointSupplier", { enumerable: true, get: function () { return EndpointSupplier_js_1.EndpointSupplier; } }); var Fetcher_js_1 = require("./Fetcher.js"); @@ -9,6 +9,8 @@ var getHeader_js_1 = require("./getHeader.js"); Object.defineProperty(exports, "getHeader", { enumerable: true, get: function () { return getHeader_js_1.getHeader; } }); var HttpResponsePromise_js_1 = require("./HttpResponsePromise.js"); Object.defineProperty(exports, "HttpResponsePromise", { enumerable: true, get: function () { return HttpResponsePromise_js_1.HttpResponsePromise; } }); +var makePassthroughRequest_js_1 = require("./makePassthroughRequest.js"); +Object.defineProperty(exports, "makePassthroughRequest", { enumerable: true, get: function () { return makePassthroughRequest_js_1.makePassthroughRequest; } }); var RawResponse_js_1 = require("./RawResponse.js"); Object.defineProperty(exports, "abortRawResponse", { enumerable: true, get: function () { return RawResponse_js_1.abortRawResponse; } }); Object.defineProperty(exports, "toRawResponse", { enumerable: true, get: function () { return RawResponse_js_1.toRawResponse; } }); diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.d.ts b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.d.ts new file mode 100644 index 000000000000..b5e202c7728a --- /dev/null +++ b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.d.ts @@ -0,0 +1,49 @@ +import { type LogConfig, type Logger } from "../logging/logger.js"; +import { Supplier } from "./Supplier.js"; +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + /** + * SDK client configuration used by the passthrough fetch method. + */ + interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export declare function makePassthroughRequest(input: Request | string | URL, init: RequestInit | undefined, clientOptions: PassthroughRequest.ClientOptions, requestOptions?: PassthroughRequest.RequestOptions): Promise; diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.js b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.js new file mode 100644 index 000000000000..a668a124520f --- /dev/null +++ b/seed/ts-sdk/exhaustive/local-files-no-source/cjs/core/fetcher/makePassthroughRequest.js @@ -0,0 +1,135 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.makePassthroughRequest = makePassthroughRequest; +const logger_js_1 = require("../logging/logger.js"); +const join_js_1 = require("../url/join.js"); +const EndpointSupplier_js_1 = require("./EndpointSupplier.js"); +const getFetchFn_js_1 = require("./getFetchFn.js"); +const makeRequest_js_1 = require("./makeRequest.js"); +const requestWithRetries_js_1 = require("./requestWithRetries.js"); +const Supplier_js_1 = require("./Supplier.js"); +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +function makePassthroughRequest(input, init, clientOptions, requestOptions) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b, _c, _d, _e, _f, _g; + const logger = (0, logger_js_1.createLogger)(clientOptions.logging); + // Extract URL and default init properties from Request object if provided + let url; + let effectiveInit = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } + else { + url = input instanceof URL ? input.toString() : input; + } + // Resolve the base URL + const baseUrl = (_a = (clientOptions.baseUrl != null ? yield Supplier_js_1.Supplier.get(clientOptions.baseUrl) : undefined)) !== null && _a !== void 0 ? _a : (clientOptions.environment != null ? yield Supplier_js_1.Supplier.get(clientOptions.environment) : undefined); + // Determine the full URL + let fullUrl; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } + else if (baseUrl != null) { + fullUrl = (0, join_js_1.join)(baseUrl, url); + } + else { + fullUrl = url; + } + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders = {}; + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = yield EndpointSupplier_js_1.EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = yield clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + // Apply user-provided headers from init + if ((effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.headers) != null) { + const initHeaders = effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + // Apply per-request option headers (highest priority) + if ((requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.headers) != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + const method = (_b = effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.method) !== null && _b !== void 0 ? _b : "GET"; + const body = effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.body; + const timeoutInSeconds = (_c = requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.timeoutInSeconds) !== null && _c !== void 0 ? _c : clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = (_d = requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.maxRetries) !== null && _d !== void 0 ? _d : clientOptions.maxRetries; + const abortSignal = (_f = (_e = requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.abortSignal) !== null && _e !== void 0 ? _e : effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.signal) !== null && _f !== void 0 ? _f : undefined; + const fetchFn = (_g = clientOptions.fetch) !== null && _g !== void 0 ? _g : (yield (0, getFetchFn_js_1.getFetchFn)()); + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + const response = yield (0, requestWithRetries_js_1.requestWithRetries)(() => __awaiter(this, void 0, void 0, function* () { + return (0, makeRequest_js_1.makeRequest)(fetchFn, fullUrl, method, mergedHeaders, body !== null && body !== void 0 ? body : undefined, timeoutMs, abortSignal, (effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.credentials) === "include", undefined, // duplex + false); + }), maxRetries); + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + return response; + }); +} diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.d.mts b/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.d.mts index a28d8fce0a22..a760b17011fb 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.d.mts +++ b/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.d.mts @@ -5,6 +5,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.mjs"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.mjs"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.mjs"; import { type NormalizedClientOptionsWithAuth } from "./BaseClient.mjs"; +import * as core from "./core/index.mjs"; export declare namespace SeedExhaustiveClient { type Options = BaseClientOptions; interface RequestOptions extends BaseRequestOptions { @@ -23,4 +24,15 @@ export declare class SeedExhaustiveClient { get noAuth(): NoAuthClient; get noReqBody(): NoReqBodyClient; get reqWithHeaders(): ReqWithHeadersClient; + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + fetch(input: Request | string | URL, init?: RequestInit, requestOptions?: core.PassthroughRequest.RequestOptions): Promise; } diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.mjs b/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.mjs index 61da6872ad1f..4ae24f89dd9a 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.mjs +++ b/seed/ts-sdk/exhaustive/local-files-no-source/esm/Client.mjs @@ -1,10 +1,20 @@ // This file was auto-generated by Fern from our API Definition. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; import { EndpointsClient } from "./api/resources/endpoints/client/Client.mjs"; import { InlinedRequestsClient } from "./api/resources/inlinedRequests/client/Client.mjs"; import { NoAuthClient } from "./api/resources/noAuth/client/Client.mjs"; import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.mjs"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.mjs"; import { normalizeClientOptionsWithAuth } from "./BaseClient.mjs"; +import * as core from "./core/index.mjs"; export class SeedExhaustiveClient { constructor(options) { this._options = normalizeClientOptionsWithAuth(options); @@ -29,4 +39,28 @@ export class SeedExhaustiveClient { var _a; return ((_a = this._reqWithHeaders) !== null && _a !== void 0 ? _a : (this._reqWithHeaders = new ReqWithHeadersClient(this._options))); } + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + fetch(input, init, requestOptions) { + return __awaiter(this, void 0, void 0, function* () { + return core.makePassthroughRequest(input, init, { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: () => __awaiter(this, void 0, void 0, function* () { return (yield this._options.authProvider.getAuthRequest()).headers; }), + }, requestOptions); + }); + } } diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.d.mts b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.d.mts index c12f8c0e6e3f..4294ddc85942 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.d.mts +++ b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.d.mts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.mjs"; export { fetcher } from "./Fetcher.mjs"; export { getHeader } from "./getHeader.mjs"; export { HttpResponsePromise } from "./HttpResponsePromise.mjs"; +export type { PassthroughRequest } from "./makePassthroughRequest.mjs"; +export { makePassthroughRequest } from "./makePassthroughRequest.mjs"; export type { RawResponse, WithRawResponse } from "./RawResponse.mjs"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.mjs"; export { Supplier } from "./Supplier.mjs"; diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.mjs b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.mjs index 5c69554a573a..28d6e21e39f4 100644 --- a/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.mjs +++ b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/index.mjs @@ -2,5 +2,6 @@ export { EndpointSupplier } from "./EndpointSupplier.mjs"; export { fetcher } from "./Fetcher.mjs"; export { getHeader } from "./getHeader.mjs"; export { HttpResponsePromise } from "./HttpResponsePromise.mjs"; +export { makePassthroughRequest } from "./makePassthroughRequest.mjs"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.mjs"; export { Supplier } from "./Supplier.mjs"; diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.d.mts b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.d.mts new file mode 100644 index 000000000000..d51c087ef704 --- /dev/null +++ b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.d.mts @@ -0,0 +1,49 @@ +import { type LogConfig, type Logger } from "../logging/logger.mjs"; +import { Supplier } from "./Supplier.mjs"; +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + /** + * SDK client configuration used by the passthrough fetch method. + */ + interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export declare function makePassthroughRequest(input: Request | string | URL, init: RequestInit | undefined, clientOptions: PassthroughRequest.ClientOptions, requestOptions?: PassthroughRequest.RequestOptions): Promise; diff --git a/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.mjs b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.mjs new file mode 100644 index 000000000000..53801d59a8a9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/local-files-no-source/esm/core/fetcher/makePassthroughRequest.mjs @@ -0,0 +1,132 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { createLogger } from "../logging/logger.mjs"; +import { join } from "../url/join.mjs"; +import { EndpointSupplier } from "./EndpointSupplier.mjs"; +import { getFetchFn } from "./getFetchFn.mjs"; +import { makeRequest } from "./makeRequest.mjs"; +import { requestWithRetries } from "./requestWithRetries.mjs"; +import { Supplier } from "./Supplier.mjs"; +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export function makePassthroughRequest(input, init, clientOptions, requestOptions) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b, _c, _d, _e, _f, _g; + const logger = createLogger(clientOptions.logging); + // Extract URL and default init properties from Request object if provided + let url; + let effectiveInit = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } + else { + url = input instanceof URL ? input.toString() : input; + } + // Resolve the base URL + const baseUrl = (_a = (clientOptions.baseUrl != null ? yield Supplier.get(clientOptions.baseUrl) : undefined)) !== null && _a !== void 0 ? _a : (clientOptions.environment != null ? yield Supplier.get(clientOptions.environment) : undefined); + // Determine the full URL + let fullUrl; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } + else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } + else { + fullUrl = url; + } + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders = {}; + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = yield EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = yield clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + // Apply user-provided headers from init + if ((effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.headers) != null) { + const initHeaders = effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + // Apply per-request option headers (highest priority) + if ((requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.headers) != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + const method = (_b = effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.method) !== null && _b !== void 0 ? _b : "GET"; + const body = effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.body; + const timeoutInSeconds = (_c = requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.timeoutInSeconds) !== null && _c !== void 0 ? _c : clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = (_d = requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.maxRetries) !== null && _d !== void 0 ? _d : clientOptions.maxRetries; + const abortSignal = (_f = (_e = requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.abortSignal) !== null && _e !== void 0 ? _e : effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.signal) !== null && _f !== void 0 ? _f : undefined; + const fetchFn = (_g = clientOptions.fetch) !== null && _g !== void 0 ? _g : (yield getFetchFn()); + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + const response = yield requestWithRetries(() => __awaiter(this, void 0, void 0, function* () { + return makeRequest(fetchFn, fullUrl, method, mergedHeaders, body !== null && body !== void 0 ? body : undefined, timeoutMs, abortSignal, (effectiveInit === null || effectiveInit === void 0 ? void 0 : effectiveInit.credentials) === "include", undefined, // duplex + false); + }), maxRetries); + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + return response; + }); +} diff --git a/seed/ts-sdk/exhaustive/local-files/Client.ts b/seed/ts-sdk/exhaustive/local-files/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/local-files/Client.ts +++ b/seed/ts-sdk/exhaustive/local-files/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/local-files/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/local-files/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/local-files/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/local-files/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/local-files/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/local-files/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/local-files/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/multiple-exports/README.md b/seed/ts-sdk/exhaustive/multiple-exports/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/multiple-exports/README.md +++ b/seed/ts-sdk/exhaustive/multiple-exports/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/multiple-exports/src/Client.ts b/seed/ts-sdk/exhaustive/multiple-exports/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/multiple-exports/src/Client.ts +++ b/seed/ts-sdk/exhaustive/multiple-exports/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/multiple-exports/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/multiple-exports/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/multiple-exports/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/multiple-exports/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/never-throw-errors/README.md b/seed/ts-sdk/exhaustive/never-throw-errors/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/never-throw-errors/README.md +++ b/seed/ts-sdk/exhaustive/never-throw-errors/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/never-throw-errors/src/Client.ts b/seed/ts-sdk/exhaustive/never-throw-errors/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/never-throw-errors/src/Client.ts +++ b/seed/ts-sdk/exhaustive/never-throw-errors/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/README.md b/seed/ts-sdk/exhaustive/no-custom-config/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/no-custom-config/README.md +++ b/seed/ts-sdk/exhaustive/no-custom-config/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/no-custom-config/src/Client.ts b/seed/ts-sdk/exhaustive/no-custom-config/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/exhaustive/no-custom-config/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/node-fetch/README.md b/seed/ts-sdk/exhaustive/node-fetch/README.md index b52ea48f6dbe..663f4be83d2c 100644 --- a/seed/ts-sdk/exhaustive/node-fetch/README.md +++ b/seed/ts-sdk/exhaustive/node-fetch/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/node-fetch/src/Client.ts b/seed/ts-sdk/exhaustive/node-fetch/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/node-fetch/src/Client.ts +++ b/seed/ts-sdk/exhaustive/node-fetch/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/node-fetch/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/node-fetch/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/node-fetch/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/node-fetch/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/output-src-only/Client.ts b/seed/ts-sdk/exhaustive/output-src-only/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/output-src-only/Client.ts +++ b/seed/ts-sdk/exhaustive/output-src-only/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/output-src-only/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/package-path/README.md b/seed/ts-sdk/exhaustive/package-path/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/package-path/README.md +++ b/seed/ts-sdk/exhaustive/package-path/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/Client.ts b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/Client.ts +++ b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..6922a2510093 --- /dev/null +++ b/seed/ts-sdk/exhaustive/package-path/src/test-packagePath/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../../../src/test-packagePath/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/README.md b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/README.md +++ b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/Client.ts b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/Client.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/parameter-naming-original-name/README.md b/seed/ts-sdk/exhaustive/parameter-naming-original-name/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-original-name/README.md +++ b/seed/ts-sdk/exhaustive/parameter-naming-original-name/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/Client.ts b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/Client.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/README.md b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/README.md +++ b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/Client.ts b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/Client.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/README.md b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/README.md +++ b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/Client.ts b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/Client.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/README.md b/seed/ts-sdk/exhaustive/retain-original-casing/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/retain-original-casing/README.md +++ b/seed/ts-sdk/exhaustive/retain-original-casing/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/src/Client.ts b/seed/ts-sdk/exhaustive/retain-original-casing/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/retain-original-casing/src/Client.ts +++ b/seed/ts-sdk/exhaustive/retain-original-casing/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/serde-layer/README.md b/seed/ts-sdk/exhaustive/serde-layer/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/README.md +++ b/seed/ts-sdk/exhaustive/serde-layer/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/Client.ts b/seed/ts-sdk/exhaustive/serde-layer/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/Client.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/use-jest/README.md b/seed/ts-sdk/exhaustive/use-jest/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/use-jest/README.md +++ b/seed/ts-sdk/exhaustive/use-jest/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/use-jest/src/Client.ts b/seed/ts-sdk/exhaustive/use-jest/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/use-jest/src/Client.ts +++ b/seed/ts-sdk/exhaustive/use-jest/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/use-jest/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/exhaustive/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/web-stream-wrapper/README.md b/seed/ts-sdk/exhaustive/web-stream-wrapper/README.md index ad36c751206a..c67cbe54dc4b 100644 --- a/seed/ts-sdk/exhaustive/web-stream-wrapper/README.md +++ b/seed/ts-sdk/exhaustive/web-stream-wrapper/README.md @@ -23,6 +23,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/exhaustive/web-stream-wrapper/src/Client.ts b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/Client.ts index 370640f5862d..6e2853bdd5da 100644 --- a/seed/ts-sdk/exhaustive/web-stream-wrapper/src/Client.ts +++ b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/Client.ts @@ -7,6 +7,7 @@ import { NoReqBodyClient } from "./api/resources/noReqBody/client/Client.js"; import { ReqWithHeadersClient } from "./api/resources/reqWithHeaders/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExhaustiveClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedExhaustiveClient { public get reqWithHeaders(): ReqWithHeadersClient { return (this._reqWithHeaders ??= new ReqWithHeadersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/web-stream-wrapper/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/web-stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/web-stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/web-stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/index.ts b/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/index.ts +++ b/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/with-audiences/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/exhaustive/with-audiences/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/exhaustive/with-audiences/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/exhaustive/with-audiences/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/extends/README.md b/seed/ts-sdk/extends/README.md index f6c26904611e..76b17716f949 100644 --- a/seed/ts-sdk/extends/README.md +++ b/seed/ts-sdk/extends/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -233,6 +234,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/extends/src/Client.ts b/seed/ts-sdk/extends/src/Client.ts index 9200ad85b80a..0425a1962097 100644 --- a/seed/ts-sdk/extends/src/Client.ts +++ b/seed/ts-sdk/extends/src/Client.ts @@ -81,4 +81,35 @@ export class SeedExtendsClient { "/extends/extended-inline-request-body", ); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/extends/src/core/fetcher/index.ts b/seed/ts-sdk/extends/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/extends/src/core/fetcher/index.ts +++ b/seed/ts-sdk/extends/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/extends/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/extends/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/extends/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/extends/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/extends/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/extra-properties/README.md b/seed/ts-sdk/extra-properties/README.md index 7242cebc3c12..3dd323b6abae 100644 --- a/seed/ts-sdk/extra-properties/README.md +++ b/seed/ts-sdk/extra-properties/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -244,6 +245,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/extra-properties/src/Client.ts b/seed/ts-sdk/extra-properties/src/Client.ts index 1eabd7ba1d44..4198920772e3 100644 --- a/seed/ts-sdk/extra-properties/src/Client.ts +++ b/seed/ts-sdk/extra-properties/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedExtraPropertiesClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedExtraPropertiesClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/extra-properties/src/core/fetcher/index.ts b/seed/ts-sdk/extra-properties/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/extra-properties/src/core/fetcher/index.ts +++ b/seed/ts-sdk/extra-properties/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/extra-properties/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/extra-properties/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/extra-properties/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-download/file-download-response-headers/README.md b/seed/ts-sdk/file-download/file-download-response-headers/README.md index e8eff58bde13..7553fbbf5113 100644 --- a/seed/ts-sdk/file-download/file-download-response-headers/README.md +++ b/seed/ts-sdk/file-download/file-download-response-headers/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -616,6 +617,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-download/file-download-response-headers/src/Client.ts b/seed/ts-sdk/file-download/file-download-response-headers/src/Client.ts index bda20577d1cc..39e91f624774 100644 --- a/seed/ts-sdk/file-download/file-download-response-headers/src/Client.ts +++ b/seed/ts-sdk/file-download/file-download-response-headers/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileDownloadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileDownloadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/index.ts b/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-download/file-download-response-headers/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-download/file-download-response-headers/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-download/file-download-response-headers/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/file-download/file-download-response-headers/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-download/no-custom-config/README.md b/seed/ts-sdk/file-download/no-custom-config/README.md index e8eff58bde13..7553fbbf5113 100644 --- a/seed/ts-sdk/file-download/no-custom-config/README.md +++ b/seed/ts-sdk/file-download/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -616,6 +617,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-download/no-custom-config/src/Client.ts b/seed/ts-sdk/file-download/no-custom-config/src/Client.ts index bda20577d1cc..39e91f624774 100644 --- a/seed/ts-sdk/file-download/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/file-download/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileDownloadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileDownloadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-download/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-download/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-download/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/file-download/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-download/stream-wrapper/README.md b/seed/ts-sdk/file-download/stream-wrapper/README.md index 14c27d3ede8c..fc624676c994 100644 --- a/seed/ts-sdk/file-download/stream-wrapper/README.md +++ b/seed/ts-sdk/file-download/stream-wrapper/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-download/stream-wrapper/src/Client.ts b/seed/ts-sdk/file-download/stream-wrapper/src/Client.ts index bda20577d1cc..39e91f624774 100644 --- a/seed/ts-sdk/file-download/stream-wrapper/src/Client.ts +++ b/seed/ts-sdk/file-download/stream-wrapper/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileDownloadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileDownloadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/index.ts b/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-download/stream-wrapper/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-download/stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-download/stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/file-download/stream-wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload-openapi/README.md b/seed/ts-sdk/file-upload-openapi/README.md index b4deba303b85..3402c1d12c1d 100644 --- a/seed/ts-sdk/file-upload-openapi/README.md +++ b/seed/ts-sdk/file-upload-openapi/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -287,6 +288,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-upload-openapi/src/Client.ts b/seed/ts-sdk/file-upload-openapi/src/Client.ts index ec4103b40c33..dfa8c2dadbd8 100644 --- a/seed/ts-sdk/file-upload-openapi/src/Client.ts +++ b/seed/ts-sdk/file-upload-openapi/src/Client.ts @@ -3,6 +3,7 @@ import { FileUploadExampleClient } from "./api/resources/fileUploadExample/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedApiClient { public get fileUploadExample(): FileUploadExampleClient { return (this._fileUploadExample ??= new FileUploadExampleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-upload-openapi/src/core/fetcher/index.ts b/seed/ts-sdk/file-upload-openapi/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-upload-openapi/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-upload-openapi/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-upload-openapi/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-upload-openapi/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-upload-openapi/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-upload-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-upload-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/file-upload-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/form-data-node16/README.md b/seed/ts-sdk/file-upload/form-data-node16/README.md index 6e629a31e398..216b9fcb5e3a 100644 --- a/seed/ts-sdk/file-upload/form-data-node16/README.md +++ b/seed/ts-sdk/file-upload/form-data-node16/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -289,6 +290,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-upload/form-data-node16/src/Client.ts b/seed/ts-sdk/file-upload/form-data-node16/src/Client.ts index 70438b9a8f9e..03d1130a67bb 100644 --- a/seed/ts-sdk/file-upload/form-data-node16/src/Client.ts +++ b/seed/ts-sdk/file-upload/form-data-node16/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileUploadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileUploadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/index.ts b/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-upload/form-data-node16/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-upload/form-data-node16/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-upload/form-data-node16/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/file-upload/form-data-node16/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/inline/README.md b/seed/ts-sdk/file-upload/inline/README.md index 6e629a31e398..216b9fcb5e3a 100644 --- a/seed/ts-sdk/file-upload/inline/README.md +++ b/seed/ts-sdk/file-upload/inline/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -289,6 +290,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-upload/inline/src/Client.ts b/seed/ts-sdk/file-upload/inline/src/Client.ts index 70438b9a8f9e..03d1130a67bb 100644 --- a/seed/ts-sdk/file-upload/inline/src/Client.ts +++ b/seed/ts-sdk/file-upload/inline/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileUploadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileUploadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-upload/inline/src/core/fetcher/index.ts b/seed/ts-sdk/file-upload/inline/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-upload/inline/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-upload/inline/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-upload/inline/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-upload/inline/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-upload/inline/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-upload/inline/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-upload/inline/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/file-upload/inline/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/README.md b/seed/ts-sdk/file-upload/no-custom-config/README.md index 6e629a31e398..216b9fcb5e3a 100644 --- a/seed/ts-sdk/file-upload/no-custom-config/README.md +++ b/seed/ts-sdk/file-upload/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -289,6 +290,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-upload/no-custom-config/src/Client.ts b/seed/ts-sdk/file-upload/no-custom-config/src/Client.ts index 70438b9a8f9e..03d1130a67bb 100644 --- a/seed/ts-sdk/file-upload/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/file-upload/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileUploadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileUploadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/serde/README.md b/seed/ts-sdk/file-upload/serde/README.md index 6e629a31e398..216b9fcb5e3a 100644 --- a/seed/ts-sdk/file-upload/serde/README.md +++ b/seed/ts-sdk/file-upload/serde/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -289,6 +290,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-upload/serde/src/Client.ts b/seed/ts-sdk/file-upload/serde/src/Client.ts index 70438b9a8f9e..03d1130a67bb 100644 --- a/seed/ts-sdk/file-upload/serde/src/Client.ts +++ b/seed/ts-sdk/file-upload/serde/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileUploadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileUploadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-upload/serde/src/core/fetcher/index.ts b/seed/ts-sdk/file-upload/serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-upload/serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-upload/serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-upload/serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-upload/serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-upload/serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/file-upload/serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/use-jest/README.md b/seed/ts-sdk/file-upload/use-jest/README.md index 6e629a31e398..216b9fcb5e3a 100644 --- a/seed/ts-sdk/file-upload/use-jest/README.md +++ b/seed/ts-sdk/file-upload/use-jest/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -289,6 +290,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-upload/use-jest/src/Client.ts b/seed/ts-sdk/file-upload/use-jest/src/Client.ts index 70438b9a8f9e..03d1130a67bb 100644 --- a/seed/ts-sdk/file-upload/use-jest/src/Client.ts +++ b/seed/ts-sdk/file-upload/use-jest/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileUploadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileUploadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/index.ts b/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-upload/use-jest/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-upload/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-upload/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/file-upload/use-jest/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/wrapper/README.md b/seed/ts-sdk/file-upload/wrapper/README.md index 6e629a31e398..216b9fcb5e3a 100644 --- a/seed/ts-sdk/file-upload/wrapper/README.md +++ b/seed/ts-sdk/file-upload/wrapper/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -289,6 +290,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/file-upload/wrapper/src/Client.ts b/seed/ts-sdk/file-upload/wrapper/src/Client.ts index 70438b9a8f9e..03d1130a67bb 100644 --- a/seed/ts-sdk/file-upload/wrapper/src/Client.ts +++ b/seed/ts-sdk/file-upload/wrapper/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedFileUploadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedFileUploadClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/index.ts b/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/index.ts +++ b/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrapper/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/file-upload/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/file-upload/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/folders/README.md b/seed/ts-sdk/folders/README.md index d957a3c0bcdc..b3beaad3ff3b 100644 --- a/seed/ts-sdk/folders/README.md +++ b/seed/ts-sdk/folders/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/folders/src/Client.ts b/seed/ts-sdk/folders/src/Client.ts index b6291a9022a5..a727d2373236 100644 --- a/seed/ts-sdk/folders/src/Client.ts +++ b/seed/ts-sdk/folders/src/Client.ts @@ -71,4 +71,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/folders/src/core/fetcher/index.ts b/seed/ts-sdk/folders/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/folders/src/core/fetcher/index.ts +++ b/seed/ts-sdk/folders/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/folders/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/folders/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/folders/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/folders/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/folders/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/header-auth-environment-variable/README.md b/seed/ts-sdk/header-auth-environment-variable/README.md index 582801ee8207..86b56f0c5a11 100644 --- a/seed/ts-sdk/header-auth-environment-variable/README.md +++ b/seed/ts-sdk/header-auth-environment-variable/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/header-auth-environment-variable/src/Client.ts b/seed/ts-sdk/header-auth-environment-variable/src/Client.ts index 5f8a72fec756..3b64e372e97b 100644 --- a/seed/ts-sdk/header-auth-environment-variable/src/Client.ts +++ b/seed/ts-sdk/header-auth-environment-variable/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedHeaderTokenEnvironmentVariableClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedHeaderTokenEnvironmentVariableClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/index.ts b/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/index.ts +++ b/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/header-auth-environment-variable/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/header-auth-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/header-auth-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/header-auth-environment-variable/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/header-auth/README.md b/seed/ts-sdk/header-auth/README.md index cb9dc00616d1..d3b24503222b 100644 --- a/seed/ts-sdk/header-auth/README.md +++ b/seed/ts-sdk/header-auth/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/header-auth/src/Client.ts b/seed/ts-sdk/header-auth/src/Client.ts index 231cdcb0bfd3..a115e706e98c 100644 --- a/seed/ts-sdk/header-auth/src/Client.ts +++ b/seed/ts-sdk/header-auth/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedHeaderTokenClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedHeaderTokenClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/header-auth/src/core/fetcher/index.ts b/seed/ts-sdk/header-auth/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/header-auth/src/core/fetcher/index.ts +++ b/seed/ts-sdk/header-auth/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/header-auth/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/header-auth/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/header-auth/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/header-auth/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/header-auth/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/header-auth/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/http-head/README.md b/seed/ts-sdk/http-head/README.md index 0f653cff8b0f..eef50cdaf95f 100644 --- a/seed/ts-sdk/http-head/README.md +++ b/seed/ts-sdk/http-head/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -240,6 +241,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/http-head/src/Client.ts b/seed/ts-sdk/http-head/src/Client.ts index 71219bd8d29e..019555893fb2 100644 --- a/seed/ts-sdk/http-head/src/Client.ts +++ b/seed/ts-sdk/http-head/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedHttpHeadClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedHttpHeadClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/http-head/src/core/fetcher/index.ts b/seed/ts-sdk/http-head/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/http-head/src/core/fetcher/index.ts +++ b/seed/ts-sdk/http-head/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/http-head/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/http-head/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/http-head/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/http-head/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/http-head/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/http-head/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/idempotency-headers/README.md b/seed/ts-sdk/idempotency-headers/README.md index d614440d4e84..fe5f6cf717e2 100644 --- a/seed/ts-sdk/idempotency-headers/README.md +++ b/seed/ts-sdk/idempotency-headers/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/idempotency-headers/src/Client.ts b/seed/ts-sdk/idempotency-headers/src/Client.ts index 2d9275247064..215024f2fe5a 100644 --- a/seed/ts-sdk/idempotency-headers/src/Client.ts +++ b/seed/ts-sdk/idempotency-headers/src/Client.ts @@ -3,6 +3,7 @@ import { PaymentClient } from "./api/resources/payment/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedIdempotencyHeadersClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedIdempotencyHeadersClient { public get payment(): PaymentClient { return (this._payment ??= new PaymentClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/idempotency-headers/src/core/fetcher/index.ts b/seed/ts-sdk/idempotency-headers/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/idempotency-headers/src/core/fetcher/index.ts +++ b/seed/ts-sdk/idempotency-headers/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/idempotency-headers/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/idempotency-headers/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/imdb/branded-string-aliases/README.md b/seed/ts-sdk/imdb/branded-string-aliases/README.md index 5c4c607da29a..3c1c6fa4ba03 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/README.md +++ b/seed/ts-sdk/imdb/branded-string-aliases/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -229,6 +230,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/Client.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/Client.ts index 2b66c3764139..5330649d1bed 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/Client.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/Client.ts @@ -3,6 +3,7 @@ import { ImdbClient } from "./api/resources/imdb/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedApiClient { public get imdb(): ImdbClient { return (this._imdb ??= new ImdbClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/index.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/index.ts +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/imdb/branded-string-aliases/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/imdb/branded-string-aliases/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/imdb/branded-string-aliases/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/imdb/branded-string-aliases/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/imdb/no-custom-config/README.md b/seed/ts-sdk/imdb/no-custom-config/README.md index 5c4c607da29a..3c1c6fa4ba03 100644 --- a/seed/ts-sdk/imdb/no-custom-config/README.md +++ b/seed/ts-sdk/imdb/no-custom-config/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -229,6 +230,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/imdb/no-custom-config/src/Client.ts b/seed/ts-sdk/imdb/no-custom-config/src/Client.ts index 2b66c3764139..5330649d1bed 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { ImdbClient } from "./api/resources/imdb/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedApiClient { public get imdb(): ImdbClient { return (this._imdb ??= new ImdbClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/imdb/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/imdb/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/imdb/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/imdb/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/imdb/omit-undefined/README.md b/seed/ts-sdk/imdb/omit-undefined/README.md index 5c4c607da29a..3c1c6fa4ba03 100644 --- a/seed/ts-sdk/imdb/omit-undefined/README.md +++ b/seed/ts-sdk/imdb/omit-undefined/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -229,6 +230,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/imdb/omit-undefined/src/Client.ts b/seed/ts-sdk/imdb/omit-undefined/src/Client.ts index 2b66c3764139..5330649d1bed 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/Client.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/Client.ts @@ -3,6 +3,7 @@ import { ImdbClient } from "./api/resources/imdb/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedApiClient { public get imdb(): ImdbClient { return (this._imdb ??= new ImdbClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/index.ts b/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/index.ts +++ b/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/imdb/omit-undefined/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/imdb/omit-undefined/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/imdb/omit-undefined/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/imdb/omit-undefined/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/inferred-auth-explicit/README.md b/seed/ts-sdk/inferred-auth-explicit/README.md index a3ffe36a4e55..cd3dee809098 100644 --- a/seed/ts-sdk/inferred-auth-explicit/README.md +++ b/seed/ts-sdk/inferred-auth-explicit/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/inferred-auth-explicit/src/Client.ts b/seed/ts-sdk/inferred-auth-explicit/src/Client.ts index eaea616ee673..092ba28dffbe 100644 --- a/seed/ts-sdk/inferred-auth-explicit/src/Client.ts +++ b/seed/ts-sdk/inferred-auth-explicit/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedInferredAuthExplicitClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedInferredAuthExplicitClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/index.ts b/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/index.ts +++ b/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-explicit/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/inferred-auth-explicit/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/inferred-auth-explicit/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-explicit/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/inferred-auth-implicit-api-key/README.md b/seed/ts-sdk/inferred-auth-implicit-api-key/README.md index a5312948dcf0..b4127acf992a 100644 --- a/seed/ts-sdk/inferred-auth-implicit-api-key/README.md +++ b/seed/ts-sdk/inferred-auth-implicit-api-key/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/inferred-auth-implicit-api-key/src/Client.ts b/seed/ts-sdk/inferred-auth-implicit-api-key/src/Client.ts index 3c0a4aa12495..c6909ed18774 100644 --- a/seed/ts-sdk/inferred-auth-implicit-api-key/src/Client.ts +++ b/seed/ts-sdk/inferred-auth-implicit-api-key/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedInferredAuthImplicitApiKeyClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedInferredAuthImplicitApiKeyClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/index.ts b/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/index.ts +++ b/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-implicit-api-key/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/inferred-auth-implicit-api-key/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/inferred-auth-implicit-api-key/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-implicit-api-key/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/inferred-auth-implicit-no-expiry/README.md b/seed/ts-sdk/inferred-auth-implicit-no-expiry/README.md index cdec8c1b257b..e1c70654f4e4 100644 --- a/seed/ts-sdk/inferred-auth-implicit-no-expiry/README.md +++ b/seed/ts-sdk/inferred-auth-implicit-no-expiry/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/Client.ts b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/Client.ts index e7938df8511c..4dfc7450f3a5 100644 --- a/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/Client.ts +++ b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedInferredAuthImplicitNoExpiryClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedInferredAuthImplicitNoExpiryClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/index.ts b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/index.ts +++ b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-implicit-no-expiry/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/inferred-auth-implicit-no-expiry/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/inferred-auth-implicit-no-expiry/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-implicit-no-expiry/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/inferred-auth-implicit/README.md b/seed/ts-sdk/inferred-auth-implicit/README.md index 4e64c4027929..86f7ad551556 100644 --- a/seed/ts-sdk/inferred-auth-implicit/README.md +++ b/seed/ts-sdk/inferred-auth-implicit/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/inferred-auth-implicit/src/Client.ts b/seed/ts-sdk/inferred-auth-implicit/src/Client.ts index a66444a952fd..8ac5b565a9b3 100644 --- a/seed/ts-sdk/inferred-auth-implicit/src/Client.ts +++ b/seed/ts-sdk/inferred-auth-implicit/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedInferredAuthImplicitClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedInferredAuthImplicitClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/index.ts b/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/index.ts +++ b/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-implicit/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/inferred-auth-implicit/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/inferred-auth-implicit/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/inferred-auth-implicit/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/license/README.md b/seed/ts-sdk/license/README.md index 14cb26894123..10f4f45d4df6 100644 --- a/seed/ts-sdk/license/README.md +++ b/seed/ts-sdk/license/README.md @@ -19,6 +19,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -215,6 +216,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/license/src/Client.ts b/seed/ts-sdk/license/src/Client.ts index 4df1117711ff..a7e0f973222e 100644 --- a/seed/ts-sdk/license/src/Client.ts +++ b/seed/ts-sdk/license/src/Client.ts @@ -61,4 +61,35 @@ export class SeedLicenseClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/license/src/core/fetcher/index.ts b/seed/ts-sdk/license/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/license/src/core/fetcher/index.ts +++ b/seed/ts-sdk/license/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/license/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/license/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/license/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/license/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/license/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/license/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/literal/README.md b/seed/ts-sdk/literal/README.md index d22d51ac7fa1..c85cc7b53bb1 100644 --- a/seed/ts-sdk/literal/README.md +++ b/seed/ts-sdk/literal/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/literal/src/Client.ts b/seed/ts-sdk/literal/src/Client.ts index 6ad37c924386..3bc0663c7d5b 100644 --- a/seed/ts-sdk/literal/src/Client.ts +++ b/seed/ts-sdk/literal/src/Client.ts @@ -7,6 +7,7 @@ import { QueryClient } from "./api/resources/query/client/Client.js"; import { ReferenceClient } from "./api/resources/reference/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedLiteralClient { export type Options = BaseClientOptions; @@ -45,4 +46,35 @@ export class SeedLiteralClient { public get reference(): ReferenceClient { return (this._reference ??= new ReferenceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/literal/src/core/fetcher/index.ts b/seed/ts-sdk/literal/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/literal/src/core/fetcher/index.ts +++ b/seed/ts-sdk/literal/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/literal/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/literal/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/literal/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/literal/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/literal/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/literals-unions/src/core/fetcher/index.ts b/seed/ts-sdk/literals-unions/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/literals-unions/src/core/fetcher/index.ts +++ b/seed/ts-sdk/literals-unions/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/literals-unions/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/literals-unions/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/literals-unions/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/literals-unions/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/literals-unions/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/literals-unions/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/README.md b/seed/ts-sdk/mixed-case/no-custom-config/README.md index 0e2ff4922325..a7230bb1006d 100644 --- a/seed/ts-sdk/mixed-case/no-custom-config/README.md +++ b/seed/ts-sdk/mixed-case/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -240,6 +241,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/mixed-case/no-custom-config/src/Client.ts b/seed/ts-sdk/mixed-case/no-custom-config/src/Client.ts index ed773c419d29..6e74cda969cb 100644 --- a/seed/ts-sdk/mixed-case/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/mixed-case/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedMixedCaseClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedMixedCaseClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/README.md b/seed/ts-sdk/mixed-case/retain-original-casing/README.md index 0e2ff4922325..a7230bb1006d 100644 --- a/seed/ts-sdk/mixed-case/retain-original-casing/README.md +++ b/seed/ts-sdk/mixed-case/retain-original-casing/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -240,6 +241,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/src/Client.ts b/seed/ts-sdk/mixed-case/retain-original-casing/src/Client.ts index ed773c419d29..6e74cda969cb 100644 --- a/seed/ts-sdk/mixed-case/retain-original-casing/src/Client.ts +++ b/seed/ts-sdk/mixed-case/retain-original-casing/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedMixedCaseClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedMixedCaseClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/index.ts b/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/index.ts +++ b/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-file-directory/README.md b/seed/ts-sdk/mixed-file-directory/README.md index 7a8e7d2a48c3..50c7fe70c19e 100644 --- a/seed/ts-sdk/mixed-file-directory/README.md +++ b/seed/ts-sdk/mixed-file-directory/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/mixed-file-directory/src/Client.ts b/seed/ts-sdk/mixed-file-directory/src/Client.ts index bd135a31150d..9255234f4d91 100644 --- a/seed/ts-sdk/mixed-file-directory/src/Client.ts +++ b/seed/ts-sdk/mixed-file-directory/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationClient } from "./api/resources/organization/client/Client.j import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedMixedFileDirectoryClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedMixedFileDirectoryClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/mixed-file-directory/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/mixed-file-directory/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/multi-line-docs/README.md b/seed/ts-sdk/multi-line-docs/README.md index 7e70b72a50e2..88974f7c8b38 100644 --- a/seed/ts-sdk/multi-line-docs/README.md +++ b/seed/ts-sdk/multi-line-docs/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/multi-line-docs/src/Client.ts b/seed/ts-sdk/multi-line-docs/src/Client.ts index adf7f639c4d0..3732ab11974c 100644 --- a/seed/ts-sdk/multi-line-docs/src/Client.ts +++ b/seed/ts-sdk/multi-line-docs/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedMultiLineDocsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedMultiLineDocsClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/multi-line-docs/src/core/fetcher/index.ts b/seed/ts-sdk/multi-line-docs/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/multi-line-docs/src/core/fetcher/index.ts +++ b/seed/ts-sdk/multi-line-docs/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/multi-line-docs/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/multi-line-docs/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/README.md b/seed/ts-sdk/multi-url-environment-no-default/README.md index 829516a0693e..15c1a14cbac7 100644 --- a/seed/ts-sdk/multi-url-environment-no-default/README.md +++ b/seed/ts-sdk/multi-url-environment-no-default/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/multi-url-environment-no-default/src/Client.ts b/seed/ts-sdk/multi-url-environment-no-default/src/Client.ts index 08e0530a5062..3beaa5fbaf7e 100644 --- a/seed/ts-sdk/multi-url-environment-no-default/src/Client.ts +++ b/seed/ts-sdk/multi-url-environment-no-default/src/Client.ts @@ -4,6 +4,7 @@ import { Ec2Client } from "./api/resources/ec2/client/Client.js"; import { S3Client } from "./api/resources/s3/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedMultiUrlEnvironmentNoDefaultClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedMultiUrlEnvironmentNoDefaultClient { public get s3(): S3Client { return (this._s3 ??= new S3Client(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/index.ts b/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/index.ts +++ b/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment/README.md b/seed/ts-sdk/multi-url-environment/README.md index f658e20a654e..45363ccf10da 100644 --- a/seed/ts-sdk/multi-url-environment/README.md +++ b/seed/ts-sdk/multi-url-environment/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/multi-url-environment/src/Client.ts b/seed/ts-sdk/multi-url-environment/src/Client.ts index 1765b0a1f1ae..aa1f0b23b4cf 100644 --- a/seed/ts-sdk/multi-url-environment/src/Client.ts +++ b/seed/ts-sdk/multi-url-environment/src/Client.ts @@ -4,6 +4,7 @@ import { Ec2Client } from "./api/resources/ec2/client/Client.js"; import { S3Client } from "./api/resources/s3/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedMultiUrlEnvironmentClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedMultiUrlEnvironmentClient { public get s3(): S3Client { return (this._s3 ??= new S3Client(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/multi-url-environment/src/core/fetcher/index.ts b/seed/ts-sdk/multi-url-environment/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/multi-url-environment/src/core/fetcher/index.ts +++ b/seed/ts-sdk/multi-url-environment/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/multi-url-environment/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/multi-url-environment/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/multiple-request-bodies/README.md b/seed/ts-sdk/multiple-request-bodies/README.md index 7f96f39282f1..8c17c5173779 100644 --- a/seed/ts-sdk/multiple-request-bodies/README.md +++ b/seed/ts-sdk/multiple-request-bodies/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -275,6 +276,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/multiple-request-bodies/src/Client.ts b/seed/ts-sdk/multiple-request-bodies/src/Client.ts index e67b630107e0..b467dae01477 100644 --- a/seed/ts-sdk/multiple-request-bodies/src/Client.ts +++ b/seed/ts-sdk/multiple-request-bodies/src/Client.ts @@ -137,4 +137,36 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/documents/upload"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/index.ts b/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/index.ts +++ b/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/multiple-request-bodies/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/multiple-request-bodies/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/multiple-request-bodies/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/multiple-request-bodies/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/no-environment/README.md b/seed/ts-sdk/no-environment/README.md index c46d9a16465c..d1957a3610f0 100644 --- a/seed/ts-sdk/no-environment/README.md +++ b/seed/ts-sdk/no-environment/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/no-environment/src/Client.ts b/seed/ts-sdk/no-environment/src/Client.ts index 79d727121009..c44911e9c0f2 100644 --- a/seed/ts-sdk/no-environment/src/Client.ts +++ b/seed/ts-sdk/no-environment/src/Client.ts @@ -3,6 +3,7 @@ import { DummyClient } from "./api/resources/dummy/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedNoEnvironmentClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedNoEnvironmentClient { public get dummy(): DummyClient { return (this._dummy ??= new DummyClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/no-environment/src/core/fetcher/index.ts b/seed/ts-sdk/no-environment/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/no-environment/src/core/fetcher/index.ts +++ b/seed/ts-sdk/no-environment/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/no-environment/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/no-environment/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/no-environment/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/no-environment/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/no-environment/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/no-retries/README.md b/seed/ts-sdk/no-retries/README.md index fc742986fa6e..7f8fb742cf28 100644 --- a/seed/ts-sdk/no-retries/README.md +++ b/seed/ts-sdk/no-retries/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/no-retries/src/Client.ts b/seed/ts-sdk/no-retries/src/Client.ts index 4cfe5e14bf0c..483811fb8c0c 100644 --- a/seed/ts-sdk/no-retries/src/Client.ts +++ b/seed/ts-sdk/no-retries/src/Client.ts @@ -3,6 +3,7 @@ import { RetriesClient } from "./api/resources/retries/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedNoRetriesClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedNoRetriesClient { public get retries(): RetriesClient { return (this._retries ??= new RetriesClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/no-retries/src/core/fetcher/index.ts b/seed/ts-sdk/no-retries/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/no-retries/src/core/fetcher/index.ts +++ b/seed/ts-sdk/no-retries/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/no-retries/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/no-retries/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/no-retries/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/no-retries/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/no-retries/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/no-retries/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/nullable-allof-extends/README.md b/seed/ts-sdk/nullable-allof-extends/README.md index 77afa2bcd13c..86ccf6cd7e5c 100644 --- a/seed/ts-sdk/nullable-allof-extends/README.md +++ b/seed/ts-sdk/nullable-allof-extends/README.md @@ -19,6 +19,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -215,6 +216,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/nullable-allof-extends/src/Client.ts b/seed/ts-sdk/nullable-allof-extends/src/Client.ts index bbba8f4e510c..eff8b692a8aa 100644 --- a/seed/ts-sdk/nullable-allof-extends/src/Client.ts +++ b/seed/ts-sdk/nullable-allof-extends/src/Client.ts @@ -123,4 +123,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/test"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/index.ts b/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/index.ts +++ b/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/nullable-allof-extends/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/nullable-allof-extends/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/nullable-allof-extends/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/nullable-allof-extends/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/nullable-optional/README.md b/seed/ts-sdk/nullable-optional/README.md index 55aceacdc632..6a75e6f0555b 100644 --- a/seed/ts-sdk/nullable-optional/README.md +++ b/seed/ts-sdk/nullable-optional/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -253,6 +254,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/nullable-optional/src/Client.ts b/seed/ts-sdk/nullable-optional/src/Client.ts index 34433bd0b8b8..66c1a8d849d6 100644 --- a/seed/ts-sdk/nullable-optional/src/Client.ts +++ b/seed/ts-sdk/nullable-optional/src/Client.ts @@ -3,6 +3,7 @@ import { NullableOptionalClient } from "./api/resources/nullableOptional/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedNullableOptionalClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedNullableOptionalClient { public get nullableOptional(): NullableOptionalClient { return (this._nullableOptional ??= new NullableOptionalClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/nullable-optional/src/core/fetcher/index.ts b/seed/ts-sdk/nullable-optional/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/nullable-optional/src/core/fetcher/index.ts +++ b/seed/ts-sdk/nullable-optional/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/nullable-optional/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/nullable-optional/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/nullable-optional/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/nullable-optional/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/nullable-optional/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/nullable-optional/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/nullable-request-body/README.md b/seed/ts-sdk/nullable-request-body/README.md index ba677a8541f3..112e30479ff2 100644 --- a/seed/ts-sdk/nullable-request-body/README.md +++ b/seed/ts-sdk/nullable-request-body/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/nullable-request-body/src/Client.ts b/seed/ts-sdk/nullable-request-body/src/Client.ts index 9cc576cc26be..bc8bc12e6ec7 100644 --- a/seed/ts-sdk/nullable-request-body/src/Client.ts +++ b/seed/ts-sdk/nullable-request-body/src/Client.ts @@ -3,6 +3,7 @@ import { TestGroupClient } from "./api/resources/testGroup/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedApiClient { public get testGroup(): TestGroupClient { return (this._testGroup ??= new TestGroupClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/nullable-request-body/src/core/fetcher/index.ts b/seed/ts-sdk/nullable-request-body/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/nullable-request-body/src/core/fetcher/index.ts +++ b/seed/ts-sdk/nullable-request-body/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/nullable-request-body/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/nullable-request-body/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/nullable-request-body/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/nullable-request-body/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/nullable-request-body/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/nullable-request-body/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/nullable/README.md b/seed/ts-sdk/nullable/README.md index 4beb72cdd05a..8b092f5755fa 100644 --- a/seed/ts-sdk/nullable/README.md +++ b/seed/ts-sdk/nullable/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -256,6 +257,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/nullable/src/Client.ts b/seed/ts-sdk/nullable/src/Client.ts index 53d28537eed4..816efb63b644 100644 --- a/seed/ts-sdk/nullable/src/Client.ts +++ b/seed/ts-sdk/nullable/src/Client.ts @@ -3,6 +3,7 @@ import { NullableClient } from "./api/resources/nullable/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedNullableClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedNullableClient { public get nullable(): NullableClient { return (this._nullable ??= new NullableClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/nullable/src/core/fetcher/index.ts b/seed/ts-sdk/nullable/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/nullable/src/core/fetcher/index.ts +++ b/seed/ts-sdk/nullable/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/nullable/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/nullable/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/nullable/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/nullable/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/nullable/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/nullable/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-custom/README.md b/seed/ts-sdk/oauth-client-credentials-custom/README.md index ffbc59bdf551..d12f4ea0fae4 100644 --- a/seed/ts-sdk/oauth-client-credentials-custom/README.md +++ b/seed/ts-sdk/oauth-client-credentials-custom/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -278,6 +279,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-custom/package.json b/seed/ts-sdk/oauth-client-credentials-custom/package.json index ad0bf621cc88..de75519a69bf 100644 --- a/seed/ts-sdk/oauth-client-credentials-custom/package.json +++ b/seed/ts-sdk/oauth-client-credentials-custom/package.json @@ -2,10 +2,7 @@ "name": "@fern/oauth-client-credentials-custom", "version": "0.0.1", "private": false, - "repository": { - "type": "git", - "url": "git+https://github.com/oauth-client-credentials-custom/fern.git" - }, + "repository": "git+https://github.com/oauth-client-credentials-custom/fern", "type": "commonjs", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.mjs", diff --git a/seed/ts-sdk/oauth-client-credentials-custom/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-custom/src/Client.ts index 00fde72e2479..05185f955a5f 100644 --- a/seed/ts-sdk/oauth-client-credentials-custom/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-custom/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedOauthClientCredentialsClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-custom/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-custom/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-custom/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-custom/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/README.md b/seed/ts-sdk/oauth-client-credentials-default/README.md index cf69d4cbe95a..5b5d9e1ef90b 100644 --- a/seed/ts-sdk/oauth-client-credentials-default/README.md +++ b/seed/ts-sdk/oauth-client-credentials-default/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -275,6 +276,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-default/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-default/src/Client.ts index 972d6a7fbcfb..9f6a556b31bf 100644 --- a/seed/ts-sdk/oauth-client-credentials-default/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-default/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsDefaultClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedOauthClientCredentialsDefaultClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/README.md b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/README.md index 21f1fa721be5..98ce3ca16e6d 100644 --- a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/README.md +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -276,6 +277,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/Client.ts index 18defb873720..5b373b656905 100644 --- a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsEnvironmentVariablesClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedOauthClientCredentialsEnvironmentVariablesClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/README.md b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/README.md index d693d3414f5e..9971e740a3a0 100644 --- a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/README.md +++ b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -276,6 +277,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/Client.ts index d94565b54e4c..f0e594e2b273 100644 --- a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/Client.ts @@ -5,6 +5,7 @@ import { NestedClient } from "./api/resources/nested/client/Client.js"; import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsMandatoryAuthClient { export type Options = BaseClientOptions; @@ -33,4 +34,36 @@ export class SeedOauthClientCredentialsMandatoryAuthClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/README.md b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/README.md index c9dfd96deb4e..146f8890dc55 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/README.md +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -276,6 +277,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/Client.ts index 00fde72e2479..05185f955a5f 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedOauthClientCredentialsClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/README.md b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/README.md index c9dfd96deb4e..146f8890dc55 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/README.md +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -276,6 +277,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/Client.ts index 00fde72e2479..05185f955a5f 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedOauthClientCredentialsClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-reference/README.md b/seed/ts-sdk/oauth-client-credentials-reference/README.md index bd5903e0c48c..1be0e2603251 100644 --- a/seed/ts-sdk/oauth-client-credentials-reference/README.md +++ b/seed/ts-sdk/oauth-client-credentials-reference/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -261,6 +262,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-reference/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-reference/src/Client.ts index dadbe6ccb6e9..f792396d0454 100644 --- a/seed/ts-sdk/oauth-client-credentials-reference/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-reference/src/Client.ts @@ -4,6 +4,7 @@ import { AuthClient } from "./api/resources/auth/client/Client.js"; import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsReferenceClient { export type Options = BaseClientOptions; @@ -27,4 +28,36 @@ export class SeedOauthClientCredentialsReferenceClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-reference/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-reference/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-reference/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-reference/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-with-variables/README.md b/seed/ts-sdk/oauth-client-credentials-with-variables/README.md index 8fc5390a7cf5..b2956dc82f00 100644 --- a/seed/ts-sdk/oauth-client-credentials-with-variables/README.md +++ b/seed/ts-sdk/oauth-client-credentials-with-variables/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -276,6 +277,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials-with-variables/src/Client.ts b/seed/ts-sdk/oauth-client-credentials-with-variables/src/Client.ts index 43090ce798e5..351a0a1f5939 100644 --- a/seed/ts-sdk/oauth-client-credentials-with-variables/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials-with-variables/src/Client.ts @@ -7,6 +7,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsWithVariablesClient { export type Options = BaseClientOptions; @@ -45,4 +46,36 @@ export class SeedOauthClientCredentialsWithVariablesClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-with-variables/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials-with-variables/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials-with-variables/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-with-variables/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/README.md b/seed/ts-sdk/oauth-client-credentials/no-custom-config/README.md index f192b65ae275..5d5c6f3a43b4 100644 --- a/seed/ts-sdk/oauth-client-credentials/no-custom-config/README.md +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -276,6 +277,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts index 00fde72e2479..c6fcdf16e06c 100644 --- a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedOauthClientCredentialsClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The URL can be a full URL or a relative path (resolved against the configured base URL). + * + * @param {string} url - The URL or path to request. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + url: string, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + url, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..35d53a3a6ac8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,165 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param url - The URL or path to request. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + url: string, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (init?.headers != null) { + const initHeaders = + init.headers instanceof Headers + ? Object.fromEntries(init.headers.entries()) + : Array.isArray(init.headers) + ? Object.fromEntries(init.headers) + : init.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = init?.method ?? "GET"; + const body = init?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? init?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + init?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..f36490719ac3 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,350 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/serde/README.md b/seed/ts-sdk/oauth-client-credentials/serde/README.md index 96316fd2f8ae..da8cef2718fb 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/README.md +++ b/seed/ts-sdk/oauth-client-credentials/serde/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -276,6 +277,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/Client.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/Client.ts index 00fde72e2479..05185f955a5f 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/Client.ts @@ -6,6 +6,7 @@ import { NestedNoAuthClient } from "./api/resources/nestedNoAuth/client/Client.j import { SimpleClient } from "./api/resources/simple/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedOauthClientCredentialsClient { export type Options = BaseClientOptions; @@ -39,4 +40,36 @@ export class SeedOauthClientCredentialsClient { public get simple(): SimpleClient { return (this._simple ??= new SimpleClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/index.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/oauth-client-credentials/serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials/serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/object/src/core/fetcher/index.ts b/seed/ts-sdk/object/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/object/src/core/fetcher/index.ts +++ b/seed/ts-sdk/object/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/object/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/object/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/object/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/object/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/object/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/objects-with-imports/src/core/fetcher/index.ts b/seed/ts-sdk/objects-with-imports/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/objects-with-imports/src/core/fetcher/index.ts +++ b/seed/ts-sdk/objects-with-imports/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/objects-with-imports/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/objects-with-imports/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/optional/README.md b/seed/ts-sdk/optional/README.md index b6d4fb205357..4064088e439b 100644 --- a/seed/ts-sdk/optional/README.md +++ b/seed/ts-sdk/optional/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -230,6 +231,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/optional/src/Client.ts b/seed/ts-sdk/optional/src/Client.ts index 22049e2b200e..8f2e489a1904 100644 --- a/seed/ts-sdk/optional/src/Client.ts +++ b/seed/ts-sdk/optional/src/Client.ts @@ -3,6 +3,7 @@ import { OptionalClient } from "./api/resources/optional/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedObjectsWithImportsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedObjectsWithImportsClient { public get optional(): OptionalClient { return (this._optional ??= new OptionalClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/optional/src/core/fetcher/index.ts b/seed/ts-sdk/optional/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/optional/src/core/fetcher/index.ts +++ b/seed/ts-sdk/optional/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/optional/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/optional/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/optional/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/optional/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/optional/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/package-yml/README.md b/seed/ts-sdk/package-yml/README.md index 57e1aea0da77..c65b0f3e9bf6 100644 --- a/seed/ts-sdk/package-yml/README.md +++ b/seed/ts-sdk/package-yml/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -229,6 +230,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/package-yml/src/Client.ts b/seed/ts-sdk/package-yml/src/Client.ts index 4d4322f94ac5..71fdc22d8f13 100644 --- a/seed/ts-sdk/package-yml/src/Client.ts +++ b/seed/ts-sdk/package-yml/src/Client.ts @@ -81,4 +81,35 @@ export class SeedPackageYmlClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/{id}/"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/package-yml/src/core/fetcher/index.ts b/seed/ts-sdk/package-yml/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/package-yml/src/core/fetcher/index.ts +++ b/seed/ts-sdk/package-yml/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/package-yml/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/package-yml/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/package-yml/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/package-yml/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/package-yml/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/pagination-custom/README.md b/seed/ts-sdk/pagination-custom/README.md index 9487d8cdfeac..4c6a4630882f 100644 --- a/seed/ts-sdk/pagination-custom/README.md +++ b/seed/ts-sdk/pagination-custom/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -284,6 +285,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/pagination-custom/src/Client.ts b/seed/ts-sdk/pagination-custom/src/Client.ts index a9c67cb79df5..db29be172e23 100644 --- a/seed/ts-sdk/pagination-custom/src/Client.ts +++ b/seed/ts-sdk/pagination-custom/src/Client.ts @@ -3,6 +3,7 @@ import { UsersClient } from "./api/resources/users/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPaginationClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedPaginationClient { public get users(): UsersClient { return (this._users ??= new UsersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/pagination-custom/src/core/fetcher/index.ts b/seed/ts-sdk/pagination-custom/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/pagination-custom/src/core/fetcher/index.ts +++ b/seed/ts-sdk/pagination-custom/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/pagination-custom/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/pagination-custom/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/pagination-custom/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/pagination-custom/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/pagination-custom/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/pagination-custom/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/pagination-uri-path/README.md b/seed/ts-sdk/pagination-uri-path/README.md index 1159d60b10bf..22992578194c 100644 --- a/seed/ts-sdk/pagination-uri-path/README.md +++ b/seed/ts-sdk/pagination-uri-path/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -262,6 +263,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/pagination-uri-path/src/Client.ts b/seed/ts-sdk/pagination-uri-path/src/Client.ts index b32ab6417e3f..62ae40c9f08c 100644 --- a/seed/ts-sdk/pagination-uri-path/src/Client.ts +++ b/seed/ts-sdk/pagination-uri-path/src/Client.ts @@ -3,6 +3,7 @@ import { UsersClient } from "./api/resources/users/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPaginationUriPathClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedPaginationUriPathClient { public get users(): UsersClient { return (this._users ??= new UsersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/pagination-uri-path/src/core/fetcher/index.ts b/seed/ts-sdk/pagination-uri-path/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/pagination-uri-path/src/core/fetcher/index.ts +++ b/seed/ts-sdk/pagination-uri-path/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/pagination-uri-path/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/pagination-uri-path/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/pagination-uri-path/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/pagination-uri-path/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/pagination-uri-path/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/pagination-uri-path/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/pagination/no-custom-config/README.md b/seed/ts-sdk/pagination/no-custom-config/README.md index 3c37ba296144..bb935601adff 100644 --- a/seed/ts-sdk/pagination/no-custom-config/README.md +++ b/seed/ts-sdk/pagination/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/pagination/no-custom-config/src/Client.ts b/seed/ts-sdk/pagination/no-custom-config/src/Client.ts index 22de688b603e..6b3cdc81f0bb 100644 --- a/seed/ts-sdk/pagination/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/pagination/no-custom-config/src/Client.ts @@ -5,6 +5,7 @@ import { InlineUsersClient } from "./api/resources/inlineUsers/client/Client.js" import { UsersClient } from "./api/resources/users/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPaginationClient { export type Options = BaseClientOptions; @@ -33,4 +34,36 @@ export class SeedPaginationClient { public get users(): UsersClient { return (this._users ??= new UsersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/pagination/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/pagination/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/pagination/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/pagination/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/pagination/page-index-semantics/README.md b/seed/ts-sdk/pagination/page-index-semantics/README.md index 3c37ba296144..bb935601adff 100644 --- a/seed/ts-sdk/pagination/page-index-semantics/README.md +++ b/seed/ts-sdk/pagination/page-index-semantics/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -316,6 +317,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/pagination/page-index-semantics/src/Client.ts b/seed/ts-sdk/pagination/page-index-semantics/src/Client.ts index 22de688b603e..6b3cdc81f0bb 100644 --- a/seed/ts-sdk/pagination/page-index-semantics/src/Client.ts +++ b/seed/ts-sdk/pagination/page-index-semantics/src/Client.ts @@ -5,6 +5,7 @@ import { InlineUsersClient } from "./api/resources/inlineUsers/client/Client.js" import { UsersClient } from "./api/resources/users/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPaginationClient { export type Options = BaseClientOptions; @@ -33,4 +34,36 @@ export class SeedPaginationClient { public get users(): UsersClient { return (this._users ??= new UsersClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/index.ts b/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/index.ts +++ b/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/pagination/page-index-semantics/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/pagination/page-index-semantics/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/pagination/page-index-semantics/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/pagination/page-index-semantics/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/no-custom-config/README.md b/seed/ts-sdk/path-parameters/no-custom-config/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/no-custom-config/README.md +++ b/seed/ts-sdk/path-parameters/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/no-custom-config/src/Client.ts b/seed/ts-sdk/path-parameters/no-custom-config/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/path-parameters/no-custom-config/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/README.md b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/README.md +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/Client.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/Client.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/README.md b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/README.md +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/Client.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/Client.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters/README.md b/seed/ts-sdk/path-parameters/no-inline-path-parameters/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters/README.md +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/Client.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/Client.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/README.md b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/README.md +++ b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/Client.ts b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/Client.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/parameter-naming-original-name/README.md b/seed/ts-sdk/path-parameters/parameter-naming-original-name/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-original-name/README.md +++ b/seed/ts-sdk/path-parameters/parameter-naming-original-name/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/Client.ts b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/Client.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/README.md b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/README.md +++ b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/Client.ts b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/Client.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/README.md b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/README.md +++ b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/Client.ts b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/Client.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/path-parameters/retain-original-casing/README.md b/seed/ts-sdk/path-parameters/retain-original-casing/README.md index 29b82a744839..05512626f0b5 100644 --- a/seed/ts-sdk/path-parameters/retain-original-casing/README.md +++ b/seed/ts-sdk/path-parameters/retain-original-casing/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -243,6 +244,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/path-parameters/retain-original-casing/src/Client.ts b/seed/ts-sdk/path-parameters/retain-original-casing/src/Client.ts index 8ca9b102ffd4..c2644877743c 100644 --- a/seed/ts-sdk/path-parameters/retain-original-casing/src/Client.ts +++ b/seed/ts-sdk/path-parameters/retain-original-casing/src/Client.ts @@ -4,6 +4,7 @@ import { OrganizationsClient } from "./api/resources/organizations/client/Client import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPathParametersClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedPathParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/index.ts b/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/index.ts +++ b/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/path-parameters/retain-original-casing/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/path-parameters/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/path-parameters/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/path-parameters/retain-original-casing/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/plain-text/README.md b/seed/ts-sdk/plain-text/README.md index 1bd5c35bf9be..7e8451293189 100644 --- a/seed/ts-sdk/plain-text/README.md +++ b/seed/ts-sdk/plain-text/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/plain-text/src/Client.ts b/seed/ts-sdk/plain-text/src/Client.ts index 33e1d80e4cce..1015780f7ff1 100644 --- a/seed/ts-sdk/plain-text/src/Client.ts +++ b/seed/ts-sdk/plain-text/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPlainTextClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedPlainTextClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/plain-text/src/core/fetcher/index.ts b/seed/ts-sdk/plain-text/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/plain-text/src/core/fetcher/index.ts +++ b/seed/ts-sdk/plain-text/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/plain-text/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/plain-text/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/plain-text/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/plain-text/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/plain-text/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/plain-text/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/property-access/generate-read-write-only-types/README.md b/seed/ts-sdk/property-access/generate-read-write-only-types/README.md index 41af3d0433ed..6a7664574632 100644 --- a/seed/ts-sdk/property-access/generate-read-write-only-types/README.md +++ b/seed/ts-sdk/property-access/generate-read-write-only-types/README.md @@ -19,6 +19,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -222,6 +223,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/property-access/generate-read-write-only-types/src/Client.ts b/seed/ts-sdk/property-access/generate-read-write-only-types/src/Client.ts index fe0eaebb4309..96dc235e804c 100644 --- a/seed/ts-sdk/property-access/generate-read-write-only-types/src/Client.ts +++ b/seed/ts-sdk/property-access/generate-read-write-only-types/src/Client.ts @@ -79,4 +79,35 @@ export class SeedPropertyAccessClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/users"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/index.ts b/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/index.ts +++ b/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/property-access/generate-read-write-only-types/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/property-access/generate-read-write-only-types/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/property-access/generate-read-write-only-types/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/property-access/generate-read-write-only-types/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/property-access/no-custom-config/README.md b/seed/ts-sdk/property-access/no-custom-config/README.md index 24cbd82b1bf6..f3abdf97146f 100644 --- a/seed/ts-sdk/property-access/no-custom-config/README.md +++ b/seed/ts-sdk/property-access/no-custom-config/README.md @@ -19,6 +19,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/property-access/no-custom-config/src/Client.ts b/seed/ts-sdk/property-access/no-custom-config/src/Client.ts index a417b2e2b46d..dc816266e294 100644 --- a/seed/ts-sdk/property-access/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/property-access/no-custom-config/src/Client.ts @@ -83,4 +83,35 @@ export class SeedPropertyAccessClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/users"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/property-access/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/property-access/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/property-access/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/property-access/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/public-object/src/Client.ts b/seed/ts-sdk/public-object/src/Client.ts index 1aa026b866ca..34ad060cb224 100644 --- a/seed/ts-sdk/public-object/src/Client.ts +++ b/seed/ts-sdk/public-object/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedPublicObjectClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedPublicObjectClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/public-object/src/core/fetcher/index.ts b/seed/ts-sdk/public-object/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/public-object/src/core/fetcher/index.ts +++ b/seed/ts-sdk/public-object/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/public-object/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/public-object/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/public-object/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/public-object/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/public-object/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/public-object/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters-openapi-as-objects/README.md b/seed/ts-sdk/query-parameters-openapi-as-objects/README.md index 8c7d67b565aa..eaec410f33f8 100644 --- a/seed/ts-sdk/query-parameters-openapi-as-objects/README.md +++ b/seed/ts-sdk/query-parameters-openapi-as-objects/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -274,6 +275,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters-openapi-as-objects/src/Client.ts b/seed/ts-sdk/query-parameters-openapi-as-objects/src/Client.ts index 480e9274f478..3220782e4595 100644 --- a/seed/ts-sdk/query-parameters-openapi-as-objects/src/Client.ts +++ b/seed/ts-sdk/query-parameters-openapi-as-objects/src/Client.ts @@ -155,4 +155,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/user/getUsername"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters-openapi-as-objects/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters-openapi-as-objects/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters-openapi-as-objects/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters-openapi-as-objects/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters-openapi/README.md b/seed/ts-sdk/query-parameters-openapi/README.md index 3be53b508d0b..3a3a257c34e1 100644 --- a/seed/ts-sdk/query-parameters-openapi/README.md +++ b/seed/ts-sdk/query-parameters-openapi/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -274,6 +275,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters-openapi/src/Client.ts b/seed/ts-sdk/query-parameters-openapi/src/Client.ts index 480e9274f478..3220782e4595 100644 --- a/seed/ts-sdk/query-parameters-openapi/src/Client.ts +++ b/seed/ts-sdk/query-parameters-openapi/src/Client.ts @@ -155,4 +155,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/user/getUsername"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters-openapi/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters-openapi/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/README.md b/seed/ts-sdk/query-parameters/no-custom-config/README.md index b176f70c25f1..39f70da30a9d 100644 --- a/seed/ts-sdk/query-parameters/no-custom-config/README.md +++ b/seed/ts-sdk/query-parameters/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -278,6 +279,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters/no-custom-config/src/Client.ts b/seed/ts-sdk/query-parameters/no-custom-config/src/Client.ts index af31f5b76c47..7fc598887f31 100644 --- a/seed/ts-sdk/query-parameters/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/query-parameters/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedQueryParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedQueryParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/README.md b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/README.md index b176f70c25f1..39f70da30a9d 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/README.md +++ b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -278,6 +279,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/Client.ts b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/Client.ts index af31f5b76c47..7fc598887f31 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/Client.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedQueryParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedQueryParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-camel-case/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/parameter-naming-original-name/README.md b/seed/ts-sdk/query-parameters/parameter-naming-original-name/README.md index b176f70c25f1..39f70da30a9d 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-original-name/README.md +++ b/seed/ts-sdk/query-parameters/parameter-naming-original-name/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -278,6 +279,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/Client.ts b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/Client.ts index af31f5b76c47..7fc598887f31 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/Client.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedQueryParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedQueryParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-original-name/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-original-name/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/README.md b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/README.md index da6f2aadd133..bdcfb0c26dae 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/README.md +++ b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -278,6 +279,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/Client.ts b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/Client.ts index af31f5b76c47..7fc598887f31 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/Client.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedQueryParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedQueryParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-snake-case/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/README.md b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/README.md index b176f70c25f1..39f70da30a9d 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/README.md +++ b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -278,6 +279,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/Client.ts b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/Client.ts index af31f5b76c47..7fc598887f31 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/Client.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedQueryParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedQueryParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters/parameter-naming-wire-value/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/serde/README.md b/seed/ts-sdk/query-parameters/serde/README.md index 36566cc9d15d..4524c49688bb 100644 --- a/seed/ts-sdk/query-parameters/serde/README.md +++ b/seed/ts-sdk/query-parameters/serde/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -278,6 +279,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/query-parameters/serde/src/Client.ts b/seed/ts-sdk/query-parameters/serde/src/Client.ts index af31f5b76c47..7fc598887f31 100644 --- a/seed/ts-sdk/query-parameters/serde/src/Client.ts +++ b/seed/ts-sdk/query-parameters/serde/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedQueryParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedQueryParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/fetcher/index.ts b/seed/ts-sdk/query-parameters/serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/query-parameters/serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/query-parameters/serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/query-parameters/serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/query-parameters/serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/query-parameters/serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/query-parameters/serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/request-parameters/flatten-request-parameters/README.md b/seed/ts-sdk/request-parameters/flatten-request-parameters/README.md index a4419c3d4e25..5301673a6a0b 100644 --- a/seed/ts-sdk/request-parameters/flatten-request-parameters/README.md +++ b/seed/ts-sdk/request-parameters/flatten-request-parameters/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/request-parameters/flatten-request-parameters/src/Client.ts b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/Client.ts index 880c695792f9..8aa11612586b 100644 --- a/seed/ts-sdk/request-parameters/flatten-request-parameters/src/Client.ts +++ b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedRequestParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedRequestParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/index.ts b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/index.ts +++ b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/request-parameters/flatten-request-parameters/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/request-parameters/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/request-parameters/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/request-parameters/flatten-request-parameters/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/request-parameters/no-custom-config/README.md b/seed/ts-sdk/request-parameters/no-custom-config/README.md index a4419c3d4e25..5301673a6a0b 100644 --- a/seed/ts-sdk/request-parameters/no-custom-config/README.md +++ b/seed/ts-sdk/request-parameters/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/request-parameters/no-custom-config/src/Client.ts b/seed/ts-sdk/request-parameters/no-custom-config/src/Client.ts index 880c695792f9..8aa11612586b 100644 --- a/seed/ts-sdk/request-parameters/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/request-parameters/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedRequestParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedRequestParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/request-parameters/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/request-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/request-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/request-parameters/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/README.md b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/README.md index a4419c3d4e25..5301673a6a0b 100644 --- a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/README.md +++ b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/Client.ts b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/Client.ts index 880c695792f9..8aa11612586b 100644 --- a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/Client.ts +++ b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedRequestParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedRequestParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/index.ts b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/index.ts +++ b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/request-parameters/use-big-int-and-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/README.md b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/README.md index a4419c3d4e25..5301673a6a0b 100644 --- a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/README.md +++ b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/Client.ts b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/Client.ts index 880c695792f9..8aa11612586b 100644 --- a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/Client.ts +++ b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedRequestParametersClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedRequestParametersClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/index.ts b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/index.ts +++ b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/request-parameters/use-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/request-parameters/use-default-request-parameter-values/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/required-nullable/README.md b/seed/ts-sdk/required-nullable/README.md index ad0590800fac..0f2cb6b27cf5 100644 --- a/seed/ts-sdk/required-nullable/README.md +++ b/seed/ts-sdk/required-nullable/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -232,6 +233,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/required-nullable/src/Client.ts b/seed/ts-sdk/required-nullable/src/Client.ts index 24c59a4115d0..e62a4868ad5c 100644 --- a/seed/ts-sdk/required-nullable/src/Client.ts +++ b/seed/ts-sdk/required-nullable/src/Client.ts @@ -149,4 +149,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/foo/{id}"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/required-nullable/src/core/fetcher/index.ts b/seed/ts-sdk/required-nullable/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/required-nullable/src/core/fetcher/index.ts +++ b/seed/ts-sdk/required-nullable/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/required-nullable/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/required-nullable/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/required-nullable/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/required-nullable/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/required-nullable/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/required-nullable/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/reserved-keywords/README.md b/seed/ts-sdk/reserved-keywords/README.md index 93eee2229382..af9f2dffc4d9 100644 --- a/seed/ts-sdk/reserved-keywords/README.md +++ b/seed/ts-sdk/reserved-keywords/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/reserved-keywords/src/Client.ts b/seed/ts-sdk/reserved-keywords/src/Client.ts index 4ed74b865ace..09b12ddbcaf0 100644 --- a/seed/ts-sdk/reserved-keywords/src/Client.ts +++ b/seed/ts-sdk/reserved-keywords/src/Client.ts @@ -3,6 +3,7 @@ import { PackageClient } from "./api/resources/package/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedNurseryApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedNurseryApiClient { public get package(): PackageClient { return (this._package ??= new PackageClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/reserved-keywords/src/core/fetcher/index.ts b/seed/ts-sdk/reserved-keywords/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/reserved-keywords/src/core/fetcher/index.ts +++ b/seed/ts-sdk/reserved-keywords/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/reserved-keywords/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/reserved-keywords/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/response-property/README.md b/seed/ts-sdk/response-property/README.md index 8ae6fe3c2a78..f1ecc417842f 100644 --- a/seed/ts-sdk/response-property/README.md +++ b/seed/ts-sdk/response-property/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/response-property/src/Client.ts b/seed/ts-sdk/response-property/src/Client.ts index a97e23c552c2..1383dd1d0ce7 100644 --- a/seed/ts-sdk/response-property/src/Client.ts +++ b/seed/ts-sdk/response-property/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedResponsePropertyClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedResponsePropertyClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/response-property/src/core/fetcher/index.ts b/seed/ts-sdk/response-property/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/response-property/src/core/fetcher/index.ts +++ b/seed/ts-sdk/response-property/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/response-property/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/response-property/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/response-property/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/response-property/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/response-property/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/server-sent-event-examples/README.md b/seed/ts-sdk/server-sent-event-examples/README.md index b48f44a800fb..89ca4a499ca5 100644 --- a/seed/ts-sdk/server-sent-event-examples/README.md +++ b/seed/ts-sdk/server-sent-event-examples/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -263,6 +264,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/server-sent-event-examples/src/Client.ts b/seed/ts-sdk/server-sent-event-examples/src/Client.ts index f776bc43103a..aa25809ac816 100644 --- a/seed/ts-sdk/server-sent-event-examples/src/Client.ts +++ b/seed/ts-sdk/server-sent-event-examples/src/Client.ts @@ -3,6 +3,7 @@ import { CompletionsClient } from "./api/resources/completions/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedServerSentEventsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedServerSentEventsClient { public get completions(): CompletionsClient { return (this._completions ??= new CompletionsClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/index.ts b/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/index.ts +++ b/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/server-sent-event-examples/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/server-sent-event-examples/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/server-sent-event-examples/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/server-sent-event-examples/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/server-sent-events/README.md b/seed/ts-sdk/server-sent-events/README.md index edce7112ca08..31a804a28ff5 100644 --- a/seed/ts-sdk/server-sent-events/README.md +++ b/seed/ts-sdk/server-sent-events/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -263,6 +264,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/server-sent-events/src/Client.ts b/seed/ts-sdk/server-sent-events/src/Client.ts index f776bc43103a..aa25809ac816 100644 --- a/seed/ts-sdk/server-sent-events/src/Client.ts +++ b/seed/ts-sdk/server-sent-events/src/Client.ts @@ -3,6 +3,7 @@ import { CompletionsClient } from "./api/resources/completions/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedServerSentEventsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedServerSentEventsClient { public get completions(): CompletionsClient { return (this._completions ??= new CompletionsClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/server-sent-events/src/core/fetcher/index.ts b/seed/ts-sdk/server-sent-events/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/server-sent-events/src/core/fetcher/index.ts +++ b/seed/ts-sdk/server-sent-events/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/server-sent-events/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/server-sent-events/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/server-sent-events/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/server-url-templating/README.md b/seed/ts-sdk/server-url-templating/README.md index e8f741427637..46163274a080 100644 --- a/seed/ts-sdk/server-url-templating/README.md +++ b/seed/ts-sdk/server-url-templating/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -232,6 +233,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/server-url-templating/src/Client.ts b/seed/ts-sdk/server-url-templating/src/Client.ts index 6b4cdaf66342..5201cadacdcc 100644 --- a/seed/ts-sdk/server-url-templating/src/Client.ts +++ b/seed/ts-sdk/server-url-templating/src/Client.ts @@ -181,4 +181,34 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/auth/token"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/server-url-templating/src/core/fetcher/index.ts b/seed/ts-sdk/server-url-templating/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/server-url-templating/src/core/fetcher/index.ts +++ b/seed/ts-sdk/server-url-templating/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/server-url-templating/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/server-url-templating/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/server-url-templating/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/server-url-templating/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/server-url-templating/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/server-url-templating/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/allow-custom-fetcher/README.md b/seed/ts-sdk/simple-api/allow-custom-fetcher/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/allow-custom-fetcher/README.md +++ b/seed/ts-sdk/simple-api/allow-custom-fetcher/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/allow-custom-fetcher/src/Client.ts b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/allow-custom-fetcher/src/Client.ts +++ b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/allow-custom-fetcher/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/allow-custom-fetcher/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/allow-custom-fetcher/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/allow-custom-fetcher/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/allow-extra-fields/README.md b/seed/ts-sdk/simple-api/allow-extra-fields/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/allow-extra-fields/README.md +++ b/seed/ts-sdk/simple-api/allow-extra-fields/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/allow-extra-fields/src/Client.ts b/seed/ts-sdk/simple-api/allow-extra-fields/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/allow-extra-fields/src/Client.ts +++ b/seed/ts-sdk/simple-api/allow-extra-fields/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/allow-extra-fields/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/allow-extra-fields/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/bundle/README.md b/seed/ts-sdk/simple-api/bundle/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/bundle/README.md +++ b/seed/ts-sdk/simple-api/bundle/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/bundle/src/Client.ts b/seed/ts-sdk/simple-api/bundle/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/bundle/src/Client.ts +++ b/seed/ts-sdk/simple-api/bundle/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/bundle/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/bundle/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/bundle/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/bundle/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/bundle/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/bundle/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/bundle/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/bundle/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/bundle/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/bundle/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/custom-package-json/README.md b/seed/ts-sdk/simple-api/custom-package-json/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/custom-package-json/README.md +++ b/seed/ts-sdk/simple-api/custom-package-json/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/custom-package-json/src/Client.ts b/seed/ts-sdk/simple-api/custom-package-json/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/custom-package-json/src/Client.ts +++ b/seed/ts-sdk/simple-api/custom-package-json/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/custom-package-json/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/custom-package-json/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/custom-package-json/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/custom-package-json/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/jsr/README.md b/seed/ts-sdk/simple-api/jsr/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/jsr/README.md +++ b/seed/ts-sdk/simple-api/jsr/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/jsr/src/Client.ts b/seed/ts-sdk/simple-api/jsr/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/jsr/src/Client.ts +++ b/seed/ts-sdk/simple-api/jsr/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/jsr/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/jsr/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/jsr/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/jsr/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/jsr/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/jsr/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/jsr/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/jsr/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/jsr/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/jsr/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/legacy-exports/README.md b/seed/ts-sdk/simple-api/legacy-exports/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/legacy-exports/README.md +++ b/seed/ts-sdk/simple-api/legacy-exports/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/legacy-exports/src/Client.ts b/seed/ts-sdk/simple-api/legacy-exports/src/Client.ts index 7a2fc59e5e73..93cd3d9822ac 100644 --- a/seed/ts-sdk/simple-api/legacy-exports/src/Client.ts +++ b/seed/ts-sdk/simple-api/legacy-exports/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient"; +import * as core from "./core"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/index.ts index 2f32091ef658..6e165a0de2cf 100644 --- a/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher"; export { fetcher } from "./Fetcher"; export { getHeader } from "./getHeader"; export { HttpResponsePromise } from "./HttpResponsePromise"; +export type { PassthroughRequest } from "./makePassthroughRequest"; +export { makePassthroughRequest } from "./makePassthroughRequest"; export type { RawResponse, WithRawResponse } from "./RawResponse"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse"; export { Supplier } from "./Supplier"; diff --git a/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..82166dcf114c --- /dev/null +++ b/seed/ts-sdk/simple-api/legacy-exports/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger"; +import { join } from "../url/join"; +import { EndpointSupplier } from "./EndpointSupplier"; +import { getFetchFn } from "./getFetchFn"; +import { makeRequest } from "./makeRequest"; +import { requestWithRetries } from "./requestWithRetries"; +import { Supplier } from "./Supplier"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/legacy-exports/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/legacy-exports/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/legacy-exports/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/no-custom-config/README.md b/seed/ts-sdk/simple-api/no-custom-config/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/no-custom-config/README.md +++ b/seed/ts-sdk/simple-api/no-custom-config/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/no-custom-config/src/Client.ts b/seed/ts-sdk/simple-api/no-custom-config/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/simple-api/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/no-linter-and-formatter/README.md b/seed/ts-sdk/simple-api/no-linter-and-formatter/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/no-linter-and-formatter/README.md +++ b/seed/ts-sdk/simple-api/no-linter-and-formatter/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/no-linter-and-formatter/src/Client.ts b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/Client.ts index 68e367690017..a40773b106db 100644 --- a/seed/ts-sdk/simple-api/no-linter-and-formatter/src/Client.ts +++ b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/Client.ts @@ -27,4 +27,28 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch(input: Request | string | URL, init?: RequestInit, requestOptions?: core.PassthroughRequest.RequestOptions): Promise { + + return core.makePassthroughRequest(input, init, { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, requestOptions); + } } diff --git a/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/no-linter-and-formatter/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/no-linter-and-formatter/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/no-linter-and-formatter/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..623d85a8aaac --- /dev/null +++ b/seed/ts-sdk/simple-api/no-linter-and-formatter/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,402 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; +import { Mock } from "vitest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers["authorization"]).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest( + "https://api.example.com", + { headers: initHeaders }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "include" }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "same-origin" }, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/no-scripts/README.md b/seed/ts-sdk/simple-api/no-scripts/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/no-scripts/README.md +++ b/seed/ts-sdk/simple-api/no-scripts/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/no-scripts/src/Client.ts b/seed/ts-sdk/simple-api/no-scripts/src/Client.ts index 68e367690017..a40773b106db 100644 --- a/seed/ts-sdk/simple-api/no-scripts/src/Client.ts +++ b/seed/ts-sdk/simple-api/no-scripts/src/Client.ts @@ -27,4 +27,28 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch(input: Request | string | URL, init?: RequestInit, requestOptions?: core.PassthroughRequest.RequestOptions): Promise { + + return core.makePassthroughRequest(input, init, { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, requestOptions); + } } diff --git a/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/no-scripts/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/no-scripts/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/no-scripts/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..623d85a8aaac --- /dev/null +++ b/seed/ts-sdk/simple-api/no-scripts/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,402 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; +import { Mock } from "vitest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers["authorization"]).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest( + "https://api.example.com", + { headers: initHeaders }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "include" }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "same-origin" }, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/oidc-token/README.md b/seed/ts-sdk/simple-api/oidc-token/README.md index d6320f3a8e57..92a20a65de42 100644 --- a/seed/ts-sdk/simple-api/oidc-token/README.md +++ b/seed/ts-sdk/simple-api/oidc-token/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/oidc-token/src/Client.ts b/seed/ts-sdk/simple-api/oidc-token/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/oidc-token/src/Client.ts +++ b/seed/ts-sdk/simple-api/oidc-token/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/oidc-token/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/oidc-token/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/oidc-token/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/oidc-token/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/omit-fern-headers/README.md b/seed/ts-sdk/simple-api/omit-fern-headers/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/omit-fern-headers/README.md +++ b/seed/ts-sdk/simple-api/omit-fern-headers/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/omit-fern-headers/src/Client.ts b/seed/ts-sdk/simple-api/omit-fern-headers/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/omit-fern-headers/src/Client.ts +++ b/seed/ts-sdk/simple-api/omit-fern-headers/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/omit-fern-headers/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/omit-fern-headers/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/omit-fern-headers/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/omit-fern-headers/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/use-oxc/README.md b/seed/ts-sdk/simple-api/use-oxc/README.md index 63f82ad01343..c35a8ad0b98a 100644 --- a/seed/ts-sdk/simple-api/use-oxc/README.md +++ b/seed/ts-sdk/simple-api/use-oxc/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -230,6 +231,30 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch( + "/v1/custom/endpoint", + { + method: "GET", + }, + { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, + }, +); + +const data = await response.json(); +``` + ### Runtime Compatibility The SDK works in the following runtimes: diff --git a/seed/ts-sdk/simple-api/use-oxc/src/Client.ts b/seed/ts-sdk/simple-api/use-oxc/src/Client.ts index 3049d3294978..833808fb2ba8 100644 --- a/seed/ts-sdk/simple-api/use-oxc/src/Client.ts +++ b/seed/ts-sdk/simple-api/use-oxc/src/Client.ts @@ -23,4 +23,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-oxc/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/use-oxc/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/use-oxc/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..8077ac6661e5 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-oxc/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; +import { Mock } from "vitest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers["authorization"]).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/use-oxfmt/README.md b/seed/ts-sdk/simple-api/use-oxfmt/README.md index 63f82ad01343..c35a8ad0b98a 100644 --- a/seed/ts-sdk/simple-api/use-oxfmt/README.md +++ b/seed/ts-sdk/simple-api/use-oxfmt/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -230,6 +231,30 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch( + "/v1/custom/endpoint", + { + method: "GET", + }, + { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, + }, +); + +const data = await response.json(); +``` + ### Runtime Compatibility The SDK works in the following runtimes: diff --git a/seed/ts-sdk/simple-api/use-oxfmt/src/Client.ts b/seed/ts-sdk/simple-api/use-oxfmt/src/Client.ts index 5ed3c9c18ab1..1320110b6822 100644 --- a/seed/ts-sdk/simple-api/use-oxfmt/src/Client.ts +++ b/seed/ts-sdk/simple-api/use-oxfmt/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { normalizeClientOptionsWithAuth, type NormalizedClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-oxfmt/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/use-oxfmt/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/use-oxfmt/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..aa4fd2ac331d --- /dev/null +++ b/seed/ts-sdk/simple-api/use-oxfmt/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; +import type { Mock } from "vitest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/use-oxlint/README.md b/seed/ts-sdk/simple-api/use-oxlint/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/use-oxlint/README.md +++ b/seed/ts-sdk/simple-api/use-oxlint/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/use-oxlint/src/Client.ts b/seed/ts-sdk/simple-api/use-oxlint/src/Client.ts index 3049d3294978..833808fb2ba8 100644 --- a/seed/ts-sdk/simple-api/use-oxlint/src/Client.ts +++ b/seed/ts-sdk/simple-api/use-oxlint/src/Client.ts @@ -23,4 +23,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-oxlint/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/use-oxlint/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/use-oxlint/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..8077ac6661e5 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-oxlint/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; +import { Mock } from "vitest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers["authorization"]).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/use-prettier-no-linter/README.md b/seed/ts-sdk/simple-api/use-prettier-no-linter/README.md index 63f82ad01343..c35a8ad0b98a 100644 --- a/seed/ts-sdk/simple-api/use-prettier-no-linter/README.md +++ b/seed/ts-sdk/simple-api/use-prettier-no-linter/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -230,6 +231,30 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch( + "/v1/custom/endpoint", + { + method: "GET", + }, + { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, + }, +); + +const data = await response.json(); +``` + ### Runtime Compatibility The SDK works in the following runtimes: diff --git a/seed/ts-sdk/simple-api/use-prettier-no-linter/src/Client.ts b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/Client.ts index 3049d3294978..833808fb2ba8 100644 --- a/seed/ts-sdk/simple-api/use-prettier-no-linter/src/Client.ts +++ b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/Client.ts @@ -23,4 +23,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-prettier-no-linter/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/use-prettier-no-linter/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/use-prettier-no-linter/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..8077ac6661e5 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-prettier-no-linter/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; +import { Mock } from "vitest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers["authorization"]).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["authorization"]).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/use-prettier/README.md b/seed/ts-sdk/simple-api/use-prettier/README.md index 63f82ad01343..c35a8ad0b98a 100644 --- a/seed/ts-sdk/simple-api/use-prettier/README.md +++ b/seed/ts-sdk/simple-api/use-prettier/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -230,6 +231,30 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch( + "/v1/custom/endpoint", + { + method: "GET", + }, + { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, + }, +); + +const data = await response.json(); +``` + ### Runtime Compatibility The SDK works in the following runtimes: diff --git a/seed/ts-sdk/simple-api/use-prettier/src/Client.ts b/seed/ts-sdk/simple-api/use-prettier/src/Client.ts index 5ed3c9c18ab1..1320110b6822 100644 --- a/seed/ts-sdk/simple-api/use-prettier/src/Client.ts +++ b/seed/ts-sdk/simple-api/use-prettier/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { normalizeClientOptionsWithAuth, type NormalizedClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-prettier/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/use-prettier/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/use-prettier/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..aa4fd2ac331d --- /dev/null +++ b/seed/ts-sdk/simple-api/use-prettier/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; +import type { Mock } from "vitest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-api/use-yarn/README.md b/seed/ts-sdk/simple-api/use-yarn/README.md index 17bed408ba41..8eeb812f2f7f 100644 --- a/seed/ts-sdk/simple-api/use-yarn/README.md +++ b/seed/ts-sdk/simple-api/use-yarn/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-api/use-yarn/src/Client.ts b/seed/ts-sdk/simple-api/use-yarn/src/Client.ts index 925b881bbb0c..7bde96b2e273 100644 --- a/seed/ts-sdk/simple-api/use-yarn/src/Client.ts +++ b/seed/ts-sdk/simple-api/use-yarn/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSimpleApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSimpleApiClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/index.ts b/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-yarn/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-api/use-yarn/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-api/use-yarn/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-api/use-yarn/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/simple-fhir/README.md b/seed/ts-sdk/simple-fhir/README.md index 4e48fc075745..9442833ce1b4 100644 --- a/seed/ts-sdk/simple-fhir/README.md +++ b/seed/ts-sdk/simple-fhir/README.md @@ -19,6 +19,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -215,6 +216,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/simple-fhir/src/Client.ts b/seed/ts-sdk/simple-fhir/src/Client.ts index fc949b7e5dcf..326e1ad3519b 100644 --- a/seed/ts-sdk/simple-fhir/src/Client.ts +++ b/seed/ts-sdk/simple-fhir/src/Client.ts @@ -69,4 +69,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/account/{account_id}"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/simple-fhir/src/core/fetcher/index.ts b/seed/ts-sdk/simple-fhir/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/simple-fhir/src/core/fetcher/index.ts +++ b/seed/ts-sdk/simple-fhir/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/simple-fhir/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/simple-fhir/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/simple-fhir/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/simple-fhir/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/simple-fhir/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/simple-fhir/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-default/README.md b/seed/ts-sdk/single-url-environment-default/README.md index 310a464bd136..752786d9a132 100644 --- a/seed/ts-sdk/single-url-environment-default/README.md +++ b/seed/ts-sdk/single-url-environment-default/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/single-url-environment-default/src/Client.ts b/seed/ts-sdk/single-url-environment-default/src/Client.ts index edb3a7a0da49..b35057a76122 100644 --- a/seed/ts-sdk/single-url-environment-default/src/Client.ts +++ b/seed/ts-sdk/single-url-environment-default/src/Client.ts @@ -3,6 +3,7 @@ import { DummyClient } from "./api/resources/dummy/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSingleUrlEnvironmentDefaultClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSingleUrlEnvironmentDefaultClient { public get dummy(): DummyClient { return (this._dummy ??= new DummyClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/single-url-environment-default/src/core/fetcher/index.ts b/seed/ts-sdk/single-url-environment-default/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/single-url-environment-default/src/core/fetcher/index.ts +++ b/seed/ts-sdk/single-url-environment-default/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/single-url-environment-default/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/single-url-environment-default/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/README.md b/seed/ts-sdk/single-url-environment-no-default/README.md index bf1706d87760..f647b401ee0c 100644 --- a/seed/ts-sdk/single-url-environment-no-default/README.md +++ b/seed/ts-sdk/single-url-environment-no-default/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/single-url-environment-no-default/src/Client.ts b/seed/ts-sdk/single-url-environment-no-default/src/Client.ts index 9a7d0d23e5b0..2a8c56899bd4 100644 --- a/seed/ts-sdk/single-url-environment-no-default/src/Client.ts +++ b/seed/ts-sdk/single-url-environment-no-default/src/Client.ts @@ -3,6 +3,7 @@ import { DummyClient } from "./api/resources/dummy/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedSingleUrlEnvironmentNoDefaultClient { export type Options = BaseClientOptions; @@ -21,4 +22,36 @@ export class SeedSingleUrlEnvironmentNoDefaultClient { public get dummy(): DummyClient { return (this._dummy ??= new DummyClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/index.ts b/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/index.ts +++ b/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/streaming-parameter/README.md b/seed/ts-sdk/streaming-parameter/README.md index 578f6cd455b9..6026b5dd0a6e 100644 --- a/seed/ts-sdk/streaming-parameter/README.md +++ b/seed/ts-sdk/streaming-parameter/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -246,6 +247,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/streaming-parameter/src/Client.ts b/seed/ts-sdk/streaming-parameter/src/Client.ts index 5ba8ae0cf7f9..76b2172a3998 100644 --- a/seed/ts-sdk/streaming-parameter/src/Client.ts +++ b/seed/ts-sdk/streaming-parameter/src/Client.ts @@ -3,6 +3,7 @@ import { DummyClient } from "./api/resources/dummy/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedStreamingClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedStreamingClient { public get dummy(): DummyClient { return (this._dummy ??= new DummyClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/streaming-parameter/src/core/fetcher/index.ts b/seed/ts-sdk/streaming-parameter/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/streaming-parameter/src/core/fetcher/index.ts +++ b/seed/ts-sdk/streaming-parameter/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/streaming-parameter/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/streaming-parameter/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/streaming-parameter/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/streaming-parameter/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/streaming-parameter/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/streaming-parameter/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/README.md b/seed/ts-sdk/streaming/no-custom-config/README.md index 9edffde82b15..b697dfc3f62e 100644 --- a/seed/ts-sdk/streaming/no-custom-config/README.md +++ b/seed/ts-sdk/streaming/no-custom-config/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -263,6 +264,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/streaming/no-custom-config/src/Client.ts b/seed/ts-sdk/streaming/no-custom-config/src/Client.ts index 5ba8ae0cf7f9..76b2172a3998 100644 --- a/seed/ts-sdk/streaming/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/streaming/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { DummyClient } from "./api/resources/dummy/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedStreamingClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedStreamingClient { public get dummy(): DummyClient { return (this._dummy ??= new DummyClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/serde-layer/README.md b/seed/ts-sdk/streaming/serde-layer/README.md index 77311217dc4f..9799cd571e20 100644 --- a/seed/ts-sdk/streaming/serde-layer/README.md +++ b/seed/ts-sdk/streaming/serde-layer/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -263,6 +264,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/streaming/serde-layer/src/Client.ts b/seed/ts-sdk/streaming/serde-layer/src/Client.ts index 5ba8ae0cf7f9..76b2172a3998 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/Client.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/Client.ts @@ -3,6 +3,7 @@ import { DummyClient } from "./api/resources/dummy/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedStreamingClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedStreamingClient { public get dummy(): DummyClient { return (this._dummy ??= new DummyClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/index.ts b/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/index.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/streaming/serde-layer/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/streaming/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/streaming/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/streaming/serde-layer/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/wrapper/README.md b/seed/ts-sdk/streaming/wrapper/README.md index 9edffde82b15..b697dfc3f62e 100644 --- a/seed/ts-sdk/streaming/wrapper/README.md +++ b/seed/ts-sdk/streaming/wrapper/README.md @@ -22,6 +22,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -263,6 +264,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/streaming/wrapper/src/Client.ts b/seed/ts-sdk/streaming/wrapper/src/Client.ts index 5ba8ae0cf7f9..76b2172a3998 100644 --- a/seed/ts-sdk/streaming/wrapper/src/Client.ts +++ b/seed/ts-sdk/streaming/wrapper/src/Client.ts @@ -3,6 +3,7 @@ import { DummyClient } from "./api/resources/dummy/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedStreamingClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedStreamingClient { public get dummy(): DummyClient { return (this._dummy ??= new DummyClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/streaming/wrapper/src/core/fetcher/index.ts b/seed/ts-sdk/streaming/wrapper/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/streaming/wrapper/src/core/fetcher/index.ts +++ b/seed/ts-sdk/streaming/wrapper/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/streaming/wrapper/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/streaming/wrapper/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/streaming/wrapper/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/streaming/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/streaming/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..91e21e0cf6b0 --- /dev/null +++ b/seed/ts-sdk/streaming/wrapper/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,397 @@ +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: jest.Mock; + + beforeEach(() => { + mockFetch = jest.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + jest.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + jest.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/trace/exhaustive/README.md b/seed/ts-sdk/trace/exhaustive/README.md index 7bd079554c6a..d7354266b09e 100644 --- a/seed/ts-sdk/trace/exhaustive/README.md +++ b/seed/ts-sdk/trace/exhaustive/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/trace/exhaustive/src/Client.ts b/seed/ts-sdk/trace/exhaustive/src/Client.ts index fb00b27d558e..6028ef4970b1 100644 --- a/seed/ts-sdk/trace/exhaustive/src/Client.ts +++ b/seed/ts-sdk/trace/exhaustive/src/Client.ts @@ -10,6 +10,7 @@ import { SyspropClient } from "./api/resources/sysprop/client/Client.js"; import { V2Client } from "./api/resources/v2/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedTraceClient { export type Options = BaseClientOptions; @@ -63,4 +64,36 @@ export class SeedTraceClient { public get sysprop(): SyspropClient { return (this._sysprop ??= new SyspropClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/trace/exhaustive/src/core/fetcher/index.ts b/seed/ts-sdk/trace/exhaustive/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/trace/exhaustive/src/core/fetcher/index.ts +++ b/seed/ts-sdk/trace/exhaustive/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/trace/exhaustive/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/trace/exhaustive/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/README.md b/seed/ts-sdk/trace/no-custom-config/README.md index c74aae600893..dca090266b4a 100644 --- a/seed/ts-sdk/trace/no-custom-config/README.md +++ b/seed/ts-sdk/trace/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/trace/no-custom-config/src/Client.ts b/seed/ts-sdk/trace/no-custom-config/src/Client.ts index fb00b27d558e..6028ef4970b1 100644 --- a/seed/ts-sdk/trace/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/trace/no-custom-config/src/Client.ts @@ -10,6 +10,7 @@ import { SyspropClient } from "./api/resources/sysprop/client/Client.js"; import { V2Client } from "./api/resources/v2/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedTraceClient { export type Options = BaseClientOptions; @@ -63,4 +64,36 @@ export class SeedTraceClient { public get sysprop(): SyspropClient { return (this._sysprop ??= new SyspropClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/trace/serde-no-throwing/README.md b/seed/ts-sdk/trace/serde-no-throwing/README.md index c74aae600893..dca090266b4a 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/README.md +++ b/seed/ts-sdk/trace/serde-no-throwing/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/Client.ts b/seed/ts-sdk/trace/serde-no-throwing/src/Client.ts index fb00b27d558e..6028ef4970b1 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/Client.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/Client.ts @@ -10,6 +10,7 @@ import { SyspropClient } from "./api/resources/sysprop/client/Client.js"; import { V2Client } from "./api/resources/v2/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedTraceClient { export type Options = BaseClientOptions; @@ -63,4 +64,36 @@ export class SeedTraceClient { public get sysprop(): SyspropClient { return (this._sysprop ??= new SyspropClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/index.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/index.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/trace/serde-no-throwing/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/trace/serde-no-throwing/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/trace/serde-no-throwing/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/trace/serde/README.md b/seed/ts-sdk/trace/serde/README.md index c74aae600893..dca090266b4a 100644 --- a/seed/ts-sdk/trace/serde/README.md +++ b/seed/ts-sdk/trace/serde/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -242,6 +243,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/trace/serde/src/Client.ts b/seed/ts-sdk/trace/serde/src/Client.ts index fb00b27d558e..6028ef4970b1 100644 --- a/seed/ts-sdk/trace/serde/src/Client.ts +++ b/seed/ts-sdk/trace/serde/src/Client.ts @@ -10,6 +10,7 @@ import { SyspropClient } from "./api/resources/sysprop/client/Client.js"; import { V2Client } from "./api/resources/v2/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedTraceClient { export type Options = BaseClientOptions; @@ -63,4 +64,36 @@ export class SeedTraceClient { public get sysprop(): SyspropClient { return (this._sysprop ??= new SyspropClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/trace/serde/src/core/fetcher/index.ts b/seed/ts-sdk/trace/serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/trace/serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/trace/serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/trace/serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/trace/serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/trace/serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/trace/serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/trace/serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/trace/serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-express-casing/README.md b/seed/ts-sdk/ts-express-casing/README.md index c24e143b7d6a..e2deea445380 100644 --- a/seed/ts-sdk/ts-express-casing/README.md +++ b/seed/ts-sdk/ts-express-casing/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -244,6 +245,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/ts-express-casing/src/Client.ts b/seed/ts-sdk/ts-express-casing/src/Client.ts index 2b66c3764139..5330649d1bed 100644 --- a/seed/ts-sdk/ts-express-casing/src/Client.ts +++ b/seed/ts-sdk/ts-express-casing/src/Client.ts @@ -3,6 +3,7 @@ import { ImdbClient } from "./api/resources/imdb/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedApiClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedApiClient { public get imdb(): ImdbClient { return (this._imdb ??= new ImdbClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/ts-express-casing/src/core/fetcher/index.ts b/seed/ts-sdk/ts-express-casing/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/ts-express-casing/src/core/fetcher/index.ts +++ b/seed/ts-sdk/ts-express-casing/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/ts-express-casing/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/ts-express-casing/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/ts-express-casing/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/ts-express-casing/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/ts-express-casing/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/ts-express-casing/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-extra-properties/README.md b/seed/ts-sdk/ts-extra-properties/README.md index d9ab3cd3dc81..ba56d5645989 100644 --- a/seed/ts-sdk/ts-extra-properties/README.md +++ b/seed/ts-sdk/ts-extra-properties/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -231,6 +232,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/ts-extra-properties/src/Client.ts b/seed/ts-sdk/ts-extra-properties/src/Client.ts index c192e0306072..3b224b1e3ff2 100644 --- a/seed/ts-sdk/ts-extra-properties/src/Client.ts +++ b/seed/ts-sdk/ts-extra-properties/src/Client.ts @@ -140,4 +140,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/user"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/fetcher/index.ts b/seed/ts-sdk/ts-extra-properties/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/fetcher/index.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/ts-extra-properties/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/ts-extra-properties/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/ts-extra-properties/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/ts-extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/ts-extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/ts-extra-properties/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-inline-types/inline/README.md b/seed/ts-sdk/ts-inline-types/inline/README.md index 1acdd4232222..978d25e3a642 100644 --- a/seed/ts-sdk/ts-inline-types/inline/README.md +++ b/seed/ts-sdk/ts-inline-types/inline/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -234,6 +235,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/ts-inline-types/inline/src/Client.ts b/seed/ts-sdk/ts-inline-types/inline/src/Client.ts index 8294232be43a..5836421f9bb9 100644 --- a/seed/ts-sdk/ts-inline-types/inline/src/Client.ts +++ b/seed/ts-sdk/ts-inline-types/inline/src/Client.ts @@ -210,4 +210,35 @@ export class SeedObjectClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/root/undiscriminated-union"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/index.ts b/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/index.ts +++ b/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/ts-inline-types/inline/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/ts-inline-types/inline/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/ts-inline-types/inline/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/ts-inline-types/inline/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/ts-inline-types/no-inline/README.md b/seed/ts-sdk/ts-inline-types/no-inline/README.md index 1acdd4232222..978d25e3a642 100644 --- a/seed/ts-sdk/ts-inline-types/no-inline/README.md +++ b/seed/ts-sdk/ts-inline-types/no-inline/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -234,6 +235,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/ts-inline-types/no-inline/src/Client.ts b/seed/ts-sdk/ts-inline-types/no-inline/src/Client.ts index 8294232be43a..5836421f9bb9 100644 --- a/seed/ts-sdk/ts-inline-types/no-inline/src/Client.ts +++ b/seed/ts-sdk/ts-inline-types/no-inline/src/Client.ts @@ -210,4 +210,35 @@ export class SeedObjectClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/root/undiscriminated-union"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/index.ts b/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/index.ts +++ b/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/ts-inline-types/no-inline/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/ts-inline-types/no-inline/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/ts-inline-types/no-inline/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/ts-inline-types/no-inline/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-union-with-response-property/README.md b/seed/ts-sdk/undiscriminated-union-with-response-property/README.md index 492af454a87a..d3c9be41cde4 100644 --- a/seed/ts-sdk/undiscriminated-union-with-response-property/README.md +++ b/seed/ts-sdk/undiscriminated-union-with-response-property/README.md @@ -19,6 +19,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -215,6 +216,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/undiscriminated-union-with-response-property/src/Client.ts b/seed/ts-sdk/undiscriminated-union-with-response-property/src/Client.ts index b996befc0f07..0174e17d9e75 100644 --- a/seed/ts-sdk/undiscriminated-union-with-response-property/src/Client.ts +++ b/seed/ts-sdk/undiscriminated-union-with-response-property/src/Client.ts @@ -118,4 +118,35 @@ export class SeedUndiscriminatedUnionWithResponsePropertyClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/unions"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/index.ts b/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/index.ts +++ b/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-union-with-response-property/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/undiscriminated-union-with-response-property/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/undiscriminated-union-with-response-property/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-union-with-response-property/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/README.md b/seed/ts-sdk/undiscriminated-unions/no-custom-config/README.md index 42cbc9b29808..ecbd2c31015a 100644 --- a/seed/ts-sdk/undiscriminated-unions/no-custom-config/README.md +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -240,6 +241,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/Client.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/Client.ts index 9aec1f76fb75..da3d2ce24651 100644 --- a/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { UnionClient } from "./api/resources/union/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedUndiscriminatedUnionsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedUndiscriminatedUnionsClient { public get union(): UnionClient { return (this._union ??= new UnionClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/README.md b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/README.md index 42cbc9b29808..ecbd2c31015a 100644 --- a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/README.md +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -240,6 +241,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/Client.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/Client.ts index 9aec1f76fb75..da3d2ce24651 100644 --- a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/Client.ts +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/Client.ts @@ -3,6 +3,7 @@ import { UnionClient } from "./api/resources/union/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedUndiscriminatedUnionsClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedUndiscriminatedUnionsClient { public get union(): UnionClient { return (this._union ??= new UnionClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/index.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/index.ts +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/unions-with-local-date/README.md b/seed/ts-sdk/unions-with-local-date/README.md index 9e9e4c3c2d75..b8393f69a129 100644 --- a/seed/ts-sdk/unions-with-local-date/README.md +++ b/seed/ts-sdk/unions-with-local-date/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/unions-with-local-date/src/Client.ts b/seed/ts-sdk/unions-with-local-date/src/Client.ts index 9a681b7ab416..32c837ebaf43 100644 --- a/seed/ts-sdk/unions-with-local-date/src/Client.ts +++ b/seed/ts-sdk/unions-with-local-date/src/Client.ts @@ -5,6 +5,7 @@ import { TypesClient } from "./api/resources/types/client/Client.js"; import { UnionClient } from "./api/resources/union/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedUnionsClient { export type Options = BaseClientOptions; @@ -33,4 +34,35 @@ export class SeedUnionsClient { public get union(): UnionClient { return (this._union ??= new UnionClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/unions-with-local-date/src/core/fetcher/index.ts b/seed/ts-sdk/unions-with-local-date/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/unions-with-local-date/src/core/fetcher/index.ts +++ b/seed/ts-sdk/unions-with-local-date/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/unions-with-local-date/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/unions-with-local-date/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/unions-with-local-date/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/unions-with-local-date/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/unions-with-local-date/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/unions-with-local-date/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/unions/README.md b/seed/ts-sdk/unions/README.md index dea7f8b476f4..64701cbbe877 100644 --- a/seed/ts-sdk/unions/README.md +++ b/seed/ts-sdk/unions/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/unions/src/Client.ts b/seed/ts-sdk/unions/src/Client.ts index 6bd12cfbeb00..d9137d1ede0d 100644 --- a/seed/ts-sdk/unions/src/Client.ts +++ b/seed/ts-sdk/unions/src/Client.ts @@ -4,6 +4,7 @@ import { BigunionClient } from "./api/resources/bigunion/client/Client.js"; import { UnionClient } from "./api/resources/union/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedUnionsClient { export type Options = BaseClientOptions; @@ -27,4 +28,35 @@ export class SeedUnionsClient { public get union(): UnionClient { return (this._union ??= new UnionClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/unions/src/core/fetcher/index.ts b/seed/ts-sdk/unions/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/unions/src/core/fetcher/index.ts +++ b/seed/ts-sdk/unions/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/unions/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/unions/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/unions/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/unions/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/unions/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/README.md b/seed/ts-sdk/unknown/no-custom-config/README.md index 0b0f96f240ae..1b81ec44c3da 100644 --- a/seed/ts-sdk/unknown/no-custom-config/README.md +++ b/seed/ts-sdk/unknown/no-custom-config/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -228,6 +229,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/unknown/no-custom-config/src/Client.ts b/seed/ts-sdk/unknown/no-custom-config/src/Client.ts index 6d2e44b9dc07..eea86a48c6a1 100644 --- a/seed/ts-sdk/unknown/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/unknown/no-custom-config/src/Client.ts @@ -3,6 +3,7 @@ import { UnknownClient } from "./api/resources/unknown/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedUnknownAsAnyClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedUnknownAsAnyClient { public get unknown(): UnknownClient { return (this._unknown ??= new UnknownClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/README.md b/seed/ts-sdk/unknown/unknown-as-any/README.md index 0b0f96f240ae..1b81ec44c3da 100644 --- a/seed/ts-sdk/unknown/unknown-as-any/README.md +++ b/seed/ts-sdk/unknown/unknown-as-any/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -228,6 +229,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/unknown/unknown-as-any/src/Client.ts b/seed/ts-sdk/unknown/unknown-as-any/src/Client.ts index 6d2e44b9dc07..eea86a48c6a1 100644 --- a/seed/ts-sdk/unknown/unknown-as-any/src/Client.ts +++ b/seed/ts-sdk/unknown/unknown-as-any/src/Client.ts @@ -3,6 +3,7 @@ import { UnknownClient } from "./api/resources/unknown/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedUnknownAsAnyClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedUnknownAsAnyClient { public get unknown(): UnknownClient { return (this._unknown ??= new UnknownClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/index.ts b/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/index.ts +++ b/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/url-form-encoded/README.md b/seed/ts-sdk/url-form-encoded/README.md index 0ff749058df5..d34742e07e34 100644 --- a/seed/ts-sdk/url-form-encoded/README.md +++ b/seed/ts-sdk/url-form-encoded/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -232,6 +233,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/url-form-encoded/src/Client.ts b/seed/ts-sdk/url-form-encoded/src/Client.ts index 6f8822632566..1a53e1a6fa18 100644 --- a/seed/ts-sdk/url-form-encoded/src/Client.ts +++ b/seed/ts-sdk/url-form-encoded/src/Client.ts @@ -75,4 +75,35 @@ export class SeedApiClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/submit"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/url-form-encoded/src/core/fetcher/index.ts b/seed/ts-sdk/url-form-encoded/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/url-form-encoded/src/core/fetcher/index.ts +++ b/seed/ts-sdk/url-form-encoded/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/url-form-encoded/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/url-form-encoded/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/url-form-encoded/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/url-form-encoded/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/url-form-encoded/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/url-form-encoded/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/validation/README.md b/seed/ts-sdk/validation/README.md index 257ff99d7db6..1aec3fa0b510 100644 --- a/seed/ts-sdk/validation/README.md +++ b/seed/ts-sdk/validation/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -234,6 +235,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/validation/src/Client.ts b/seed/ts-sdk/validation/src/Client.ts index ce125a6d0517..c93a651521fd 100644 --- a/seed/ts-sdk/validation/src/Client.ts +++ b/seed/ts-sdk/validation/src/Client.ts @@ -134,4 +134,35 @@ export class SeedValidationClient { return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/"); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/validation/src/core/fetcher/index.ts b/seed/ts-sdk/validation/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/validation/src/core/fetcher/index.ts +++ b/seed/ts-sdk/validation/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/validation/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/validation/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/validation/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/validation/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/validation/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/variables/README.md b/seed/ts-sdk/variables/README.md index 0fa5791844a3..c0b894fc1215 100644 --- a/seed/ts-sdk/variables/README.md +++ b/seed/ts-sdk/variables/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/variables/src/Client.ts b/seed/ts-sdk/variables/src/Client.ts index 7c3520ad5027..3286d5a1f6e6 100644 --- a/seed/ts-sdk/variables/src/Client.ts +++ b/seed/ts-sdk/variables/src/Client.ts @@ -3,6 +3,7 @@ import { ServiceClient } from "./api/resources/service/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedVariablesClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedVariablesClient { public get service(): ServiceClient { return (this._service ??= new ServiceClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/variables/src/core/fetcher/index.ts b/seed/ts-sdk/variables/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/variables/src/core/fetcher/index.ts +++ b/seed/ts-sdk/variables/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/variables/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/variables/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/variables/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/variables/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/variables/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/variables/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/version-no-default/README.md b/seed/ts-sdk/version-no-default/README.md index e3c678adde97..c2a8b2606661 100644 --- a/seed/ts-sdk/version-no-default/README.md +++ b/seed/ts-sdk/version-no-default/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/version-no-default/src/Client.ts b/seed/ts-sdk/version-no-default/src/Client.ts index 06a46fdbc64f..12ad500e02a8 100644 --- a/seed/ts-sdk/version-no-default/src/Client.ts +++ b/seed/ts-sdk/version-no-default/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedVersionClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedVersionClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/version-no-default/src/core/fetcher/index.ts b/seed/ts-sdk/version-no-default/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/version-no-default/src/core/fetcher/index.ts +++ b/seed/ts-sdk/version-no-default/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/version-no-default/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/version-no-default/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/version-no-default/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/version-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/version-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/version-no-default/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/version/README.md b/seed/ts-sdk/version/README.md index 7999d3224d2c..ad4550cdfed3 100644 --- a/seed/ts-sdk/version/README.md +++ b/seed/ts-sdk/version/README.md @@ -20,6 +20,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -226,6 +227,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/version/src/Client.ts b/seed/ts-sdk/version/src/Client.ts index 06a46fdbc64f..12ad500e02a8 100644 --- a/seed/ts-sdk/version/src/Client.ts +++ b/seed/ts-sdk/version/src/Client.ts @@ -3,6 +3,7 @@ import { UserClient } from "./api/resources/user/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptions, normalizeClientOptions } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedVersionClient { export type Options = BaseClientOptions; @@ -21,4 +22,35 @@ export class SeedVersionClient { public get user(): UserClient { return (this._user ??= new UserClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/version/src/core/fetcher/index.ts b/seed/ts-sdk/version/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/version/src/core/fetcher/index.ts +++ b/seed/ts-sdk/version/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/version/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/version/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/version/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/version/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/version/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/version/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/webhook-audience/src/core/fetcher/index.ts b/seed/ts-sdk/webhook-audience/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/webhook-audience/src/core/fetcher/index.ts +++ b/seed/ts-sdk/webhook-audience/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/webhook-audience/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/webhook-audience/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/webhook-audience/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/webhook-audience/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/webhook-audience/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/webhook-audience/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/index.ts b/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/index.ts +++ b/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/webhooks/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/webhooks/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/webhooks/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/webhooks/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/index.ts b/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/index.ts +++ b/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/websocket-bearer-auth/websockets/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/websocket-bearer-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/websocket-bearer-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/websocket-bearer-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/websocket-inferred-auth/websockets/README.md b/seed/ts-sdk/websocket-inferred-auth/websockets/README.md index fdfcec80f178..14056b1c18df 100644 --- a/seed/ts-sdk/websocket-inferred-auth/websockets/README.md +++ b/seed/ts-sdk/websocket-inferred-auth/websockets/README.md @@ -21,6 +21,7 @@ The Seed TypeScript library provides convenient access to the Seed APIs from Typ - [Aborting Requests](#aborting-requests) - [Access Raw Response Data](#access-raw-response-data) - [Logging](#logging) + - [Custom Fetch](#custom-fetch) - [Runtime Compatibility](#runtime-compatibility) - [Contributing](#contributing) @@ -245,6 +246,26 @@ const logger: logging.ILogger = { +### Custom Fetch + +The SDK provides a low-level `fetch` method for making custom HTTP requests while still +benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. +This is useful for calling API endpoints not yet supported in the SDK. + +```typescript +const response = await client.fetch("/v1/custom/endpoint", { + method: "GET", +}, { + timeoutInSeconds: 30, + maxRetries: 3, + headers: { + "X-Custom-Header": "custom-value", + }, +}); + +const data = await response.json(); +``` + ### Runtime Compatibility diff --git a/seed/ts-sdk/websocket-inferred-auth/websockets/src/Client.ts b/seed/ts-sdk/websocket-inferred-auth/websockets/src/Client.ts index 1fbf07111266..48368aff9566 100644 --- a/seed/ts-sdk/websocket-inferred-auth/websockets/src/Client.ts +++ b/seed/ts-sdk/websocket-inferred-auth/websockets/src/Client.ts @@ -4,6 +4,7 @@ import { AuthClient } from "./api/resources/auth/client/Client.js"; import { RealtimeClient } from "./api/resources/realtime/client/Client.js"; import type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js"; import { type NormalizedClientOptionsWithAuth, normalizeClientOptionsWithAuth } from "./BaseClient.js"; +import * as core from "./core/index.js"; export declare namespace SeedWebsocketAuthClient { export type Options = BaseClientOptions; @@ -27,4 +28,36 @@ export class SeedWebsocketAuthClient { public get realtime(): RealtimeClient { return (this._realtime ??= new RealtimeClient(this._options)); } + + /** + * Make a passthrough request using the SDK's configured auth, retry, logging, etc. + * This is useful for making requests to endpoints not yet supported in the SDK. + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. + * + * @param {Request | string | URL} input - The URL, path, or Request object. + * @param {RequestInit} init - Standard fetch RequestInit options. + * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). + * @returns {Promise} A standard Response object. + */ + public async fetch( + input: Request | string | URL, + init?: RequestInit, + requestOptions?: core.PassthroughRequest.RequestOptions, + ): Promise { + return core.makePassthroughRequest( + input, + init, + { + environment: this._options.environment, + baseUrl: this._options.baseUrl, + headers: this._options.headers, + timeoutInSeconds: this._options.timeoutInSeconds, + maxRetries: this._options.maxRetries, + fetch: this._options.fetch, + logging: this._options.logging, + getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers, + }, + requestOptions, + ); + } } diff --git a/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/index.ts b/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/index.ts +++ b/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/websocket-inferred-auth/websockets/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/websocket-inferred-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/websocket-inferred-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/websocket-inferred-auth/websockets/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/websocket/no-serde/src/core/fetcher/index.ts b/seed/ts-sdk/websocket/no-serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/websocket/no-serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/websocket/no-serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/websocket/no-serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/websocket/no-serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/websocket/no-serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/websocket/no-serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/websocket/no-serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/websocket/no-serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/index.ts b/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/index.ts +++ b/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/websocket/no-websocket-clients/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/websocket/no-websocket-clients/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/websocket/no-websocket-clients/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/websocket/no-websocket-clients/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); diff --git a/seed/ts-sdk/websocket/serde/src/core/fetcher/index.ts b/seed/ts-sdk/websocket/serde/src/core/fetcher/index.ts index c3bc6da20f49..bd5db362c778 100644 --- a/seed/ts-sdk/websocket/serde/src/core/fetcher/index.ts +++ b/seed/ts-sdk/websocket/serde/src/core/fetcher/index.ts @@ -6,6 +6,8 @@ export type { Fetcher, FetchFunction } from "./Fetcher.js"; export { fetcher } from "./Fetcher.js"; export { getHeader } from "./getHeader.js"; export { HttpResponsePromise } from "./HttpResponsePromise.js"; +export type { PassthroughRequest } from "./makePassthroughRequest.js"; +export { makePassthroughRequest } from "./makePassthroughRequest.js"; export type { RawResponse, WithRawResponse } from "./RawResponse.js"; export { abortRawResponse, toRawResponse, unknownRawResponse } from "./RawResponse.js"; export { Supplier } from "./Supplier.js"; diff --git a/seed/ts-sdk/websocket/serde/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/websocket/serde/src/core/fetcher/makePassthroughRequest.ts new file mode 100644 index 000000000000..f5ba761400f8 --- /dev/null +++ b/seed/ts-sdk/websocket/serde/src/core/fetcher/makePassthroughRequest.ts @@ -0,0 +1,189 @@ +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; +import { join } from "../url/join.js"; +import { EndpointSupplier } from "./EndpointSupplier.js"; +import { getFetchFn } from "./getFetchFn.js"; +import { makeRequest } from "./makeRequest.js"; +import { requestWithRetries } from "./requestWithRetries.js"; +import { Supplier } from "./Supplier.js"; + +export declare namespace PassthroughRequest { + /** + * Per-request options that can override the SDK client defaults. + */ + export interface RequestOptions { + /** Override the default timeout for this request (in seconds). */ + timeoutInSeconds?: number; + /** Override the default number of retries for this request. */ + maxRetries?: number; + /** Additional headers to include in this request. */ + headers?: Record; + /** Abort signal for this request. */ + abortSignal?: AbortSignal; + } + + /** + * SDK client configuration used by the passthrough fetch method. + */ + export interface ClientOptions { + /** The base URL or environment for the client. */ + environment?: Supplier; + /** Override the base URL. */ + baseUrl?: Supplier; + /** Default headers to include in requests. */ + headers?: Record; + /** Default maximum time to wait for a response in seconds. */ + timeoutInSeconds?: number; + /** Default number of times to retry the request. Defaults to 2. */ + maxRetries?: number; + /** A custom fetch function. */ + fetch?: typeof fetch; + /** Logging configuration. */ + logging?: LogConfig | Logger; + /** A function that returns auth headers. */ + getAuthHeaders?: () => Promise>; + } +} + +/** + * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) + * while mimicking the standard `fetch` API. + * + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. + * @param init - Standard RequestInit options (method, headers, body, signal, etc.) + * @param clientOptions - SDK client options (auth, default headers, logging, etc.) + * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). + * @returns A standard Response object. + */ +export async function makePassthroughRequest( + input: Request | string | URL, + init: RequestInit | undefined, + clientOptions: PassthroughRequest.ClientOptions, + requestOptions?: PassthroughRequest.RequestOptions, +): Promise { + const logger = createLogger(clientOptions.logging); + + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + + // Resolve the base URL + const baseUrl = + (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? + (clientOptions.environment != null ? await Supplier.get(clientOptions.environment) : undefined); + + // Determine the full URL + let fullUrl: string; + if (url.startsWith("http://") || url.startsWith("https://")) { + fullUrl = url; + } else if (baseUrl != null) { + fullUrl = join(baseUrl, url); + } else { + fullUrl = url; + } + + // Merge headers: SDK default headers -> auth headers -> user-provided headers + const mergedHeaders: Record = {}; + + // Apply SDK default headers (resolve suppliers) + if (clientOptions.headers != null) { + for (const [key, value] of Object.entries(clientOptions.headers)) { + const resolved = await EndpointSupplier.get(value, { endpointMetadata: {} }); + if (resolved != null) { + mergedHeaders[key.toLowerCase()] = `${resolved}`; + } + } + } + + // Apply auth headers + if (clientOptions.getAuthHeaders != null) { + const authHeaders = await clientOptions.getAuthHeaders(); + for (const [key, value] of Object.entries(authHeaders)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + // Apply user-provided headers from init + if (effectiveInit?.headers != null) { + const initHeaders = + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; + for (const [key, value] of Object.entries(initHeaders)) { + if (value != null) { + mergedHeaders[key.toLowerCase()] = value; + } + } + } + + // Apply per-request option headers (highest priority) + if (requestOptions?.headers != null) { + for (const [key, value] of Object.entries(requestOptions.headers)) { + mergedHeaders[key.toLowerCase()] = value; + } + } + + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; + const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; + const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; + const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; + const fetchFn = clientOptions.fetch ?? (await getFetchFn()); + + if (logger.isDebug()) { + logger.debug("Making passthrough HTTP request", { + method, + url: fullUrl, + hasBody: body != null, + }); + } + + const response = await requestWithRetries( + async () => + makeRequest( + fetchFn, + fullUrl, + method, + mergedHeaders, + body ?? undefined, + timeoutMs, + abortSignal, + effectiveInit?.credentials === "include", + undefined, // duplex + false, // disableCache + ), + maxRetries, + ); + + if (logger.isDebug()) { + logger.debug("Passthrough HTTP request completed", { + method, + url: fullUrl, + statusCode: response.status, + }); + } + + return response; +} diff --git a/seed/ts-sdk/websocket/serde/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/websocket/serde/tests/unit/fetcher/makePassthroughRequest.test.ts new file mode 100644 index 000000000000..1850d1fda959 --- /dev/null +++ b/seed/ts-sdk/websocket/serde/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -0,0 +1,398 @@ +import type { Mock } from "vitest"; +import { makePassthroughRequest } from "../../../src/core/fetcher/makePassthroughRequest"; + +describe("makePassthroughRequest", () => { + let mockFetch: Mock; + + beforeEach(() => { + mockFetch = vi.fn(); + mockFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }); + + describe("URL resolution", () => { + it("should use absolute URL directly", async () => { + await makePassthroughRequest("https://api.example.com/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against baseUrl", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://api.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + + it("should resolve relative path against environment when baseUrl is not set", async () => { + await makePassthroughRequest("/v1/users", undefined, { + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://env.example.com/v1/users"); + }); + + it("should prefer baseUrl over environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: "https://base.example.com", + environment: "https://env.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://base.example.com/v1/users"); + }); + + it("should pass relative URL through as-is when no baseUrl or environment", async () => { + await makePassthroughRequest("/v1/users", undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("/v1/users"); + }); + + it("should resolve baseUrl supplier", async () => { + await makePassthroughRequest("/v1/users", undefined, { + baseUrl: () => "https://dynamic.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://dynamic.example.com/v1/users"); + }); + + it("should ignore absolute URL even when baseUrl is set", async () => { + await makePassthroughRequest("https://other.example.com/path", undefined, { + baseUrl: "https://base.example.com", + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://other.example.com/path"); + }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); + }); + + describe("header merge order", () => { + it("should merge headers in correct priority: SDK defaults < auth < init < requestOptions", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "X-Custom": "from-init", Authorization: "from-init" }, + }, + { + headers: { + "X-Custom": "from-sdk", + "X-SDK-Only": "sdk-value", + Authorization: "from-sdk", + }, + getAuthHeaders: async () => ({ + Authorization: "Bearer auth-token", + "X-Auth-Only": "auth-value", + }), + fetch: mockFetch, + }, + { + headers: { Authorization: "from-request-options" }, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + + // requestOptions.headers wins for Authorization (highest priority) + expect(headers.authorization).toBe("from-request-options"); + // init.headers wins over SDK defaults for X-Custom + expect(headers["x-custom"]).toBe("from-init"); + // SDK-only header is preserved + expect(headers["x-sdk-only"]).toBe("sdk-value"); + // Auth-only header is preserved + expect(headers["x-auth-only"]).toBe("auth-value"); + }); + + it("should lowercase all header keys", async () => { + await makePassthroughRequest( + "https://api.example.com", + { + headers: { "Content-Type": "application/json" }, + }, + { + headers: { "X-Fern-Language": "JavaScript" }, + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + const headers = calledOptions.headers; + expect(headers["content-type"]).toBe("application/json"); + expect(headers["x-fern-language"]).toBe("JavaScript"); + expect(headers["Content-Type"]).toBeUndefined(); + expect(headers["X-Fern-Language"]).toBeUndefined(); + }); + + it("should handle Headers object in init", async () => { + const initHeaders = new Headers(); + initHeaders.set("X-From-Headers-Object", "value"); + await makePassthroughRequest("https://api.example.com", { headers: initHeaders }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-headers-object"]).toBe("value"); + }); + + it("should handle array-style headers in init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: [["X-Array-Header", "array-value"]] }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-array-header"]).toBe("array-value"); + }); + + it("should skip null SDK default header values", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { "X-Present": "value", "X-Null": null }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-present"]).toBe("value"); + expect(calledOptions.headers["x-null"]).toBeUndefined(); + }); + }); + + describe("auth headers", () => { + it("should include auth headers when getAuthHeaders is provided", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + getAuthHeaders: async () => ({ Authorization: "Bearer my-token" }), + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer my-token"); + }); + + it("should work without auth headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBeUndefined(); + }); + + it("should allow init headers to override auth headers", async () => { + await makePassthroughRequest( + "https://api.example.com", + { headers: { Authorization: "Bearer override" } }, + { + getAuthHeaders: async () => ({ Authorization: "Bearer sdk-auth" }), + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers.authorization).toBe("Bearer override"); + }); + }); + + describe("method and body", () => { + it("should default to GET when no method specified", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("GET"); + }); + + it("should use the method from init", async () => { + await makePassthroughRequest( + "https://api.example.com", + { method: "POST", body: JSON.stringify({ key: "value" }) }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("POST"); + expect(calledOptions.body).toBe(JSON.stringify({ key: "value" })); + }); + + it("should pass body as undefined when not provided", async () => { + await makePassthroughRequest("https://api.example.com", { method: "GET" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.body).toBeUndefined(); + }); + }); + + describe("timeout and retries", () => { + it("should use requestOptions timeout over client timeout", async () => { + await makePassthroughRequest( + "https://api.example.com", + undefined, + { timeoutInSeconds: 30, fetch: mockFetch }, + { timeoutInSeconds: 10 }, + ); + // The timeout is passed to makeRequest which converts to ms + // We verify via the signal timing behavior (indirectly tested through makeRequest) + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use client timeout when requestOptions timeout is not set", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + timeoutInSeconds: 30, + fetch: mockFetch, + }); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should use requestOptions maxRetries over client maxRetries", async () => { + mockFetch.mockResolvedValue(new Response("", { status: 500 })); + vi.spyOn(global, "setTimeout").mockImplementation((callback: (args: void) => void) => { + process.nextTick(callback); + return null as any; + }); + + await makePassthroughRequest( + "https://api.example.com", + undefined, + { maxRetries: 5, fetch: mockFetch }, + { maxRetries: 1 }, + ); + // 1 initial + 1 retry = 2 calls + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.restoreAllMocks(); + }); + }); + + describe("abort signal", () => { + it("should use requestOptions.abortSignal over init.signal", async () => { + const initController = new AbortController(); + const requestController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + { abortSignal: requestController.signal }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + // The signal passed to makeRequest is combined with timeout signal via anySignal, + // but the requestOptions.abortSignal should be the one that's used (not init.signal) + expect(calledOptions.signal).toBeDefined(); + }); + + it("should use init.signal when requestOptions.abortSignal is not set", async () => { + const initController = new AbortController(); + + await makePassthroughRequest( + "https://api.example.com", + { signal: initController.signal }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.signal).toBeDefined(); + }); + }); + + describe("credentials", () => { + it("should pass credentials include when set", async () => { + await makePassthroughRequest("https://api.example.com", { credentials: "include" }, { fetch: mockFetch }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBe("include"); + }); + + it("should not pass credentials when not set to include", async () => { + await makePassthroughRequest( + "https://api.example.com", + { credentials: "same-origin" }, + { + fetch: mockFetch, + }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.credentials).toBeUndefined(); + }); + }); + + describe("response", () => { + it("should return the Response object from fetch", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + mockFetch.mockResolvedValue(mockResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response).toBe(mockResponse); + expect(response.status).toBe(200); + }); + + it("should return error responses without throwing", async () => { + const errorResponse = new Response("Not Found", { status: 404 }); + mockFetch.mockResolvedValue(errorResponse); + + const response = await makePassthroughRequest("https://api.example.com", undefined, { + fetch: mockFetch, + }); + expect(response.status).toBe(404); + }); + }); + + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + + describe("SDK default header suppliers", () => { + it("should resolve supplier functions for SDK default headers", async () => { + await makePassthroughRequest("https://api.example.com", undefined, { + headers: { + "X-Static": "static-value", + "X-Dynamic": () => "dynamic-value", + }, + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-static"]).toBe("static-value"); + expect(calledOptions.headers["x-dynamic"]).toBe("dynamic-value"); + }); + }); +}); From f8c7af1ec3bf9d0f7b44beb2c46850064c509ee8 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:02:45 -0400 Subject: [PATCH 28/29] feat(cli): add AI changelog rollup with version_bump_reason field (#13469) Co-authored-by: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/cli/ai/baml_src/diff_analyzer.baml | 77 +++++++++- .../cli/ai/src/baml_client/async_client.ts | 132 +++++++++++++++++- .../cli/ai/src/baml_client/async_request.ts | 68 ++++++++- .../cli/ai/src/baml_client/inlinedbaml.ts | 2 +- packages/cli/ai/src/baml_client/parser.ts | 48 ++++++- .../cli/ai/src/baml_client/partial_types.ts | 8 +- .../cli/ai/src/baml_client/sync_client.ts | 52 ++++++- .../cli/ai/src/baml_client/sync_request.ts | 68 ++++++++- .../cli/ai/src/baml_client/type_builder.ts | 12 +- packages/cli/ai/src/baml_client/types.ts | 8 ++ .../src/commands/sdk-diff/sdkDiffCommand.ts | 35 ++++- packages/cli/cli/versions.yml | 17 ++- .../src/AutoVersioningCache.ts | 4 + .../src/AutoVersioningService.ts | 10 ++ .../src/GenerationRunner.ts | 2 + .../src/LocalTaskHandler.ts | 61 ++++++-- .../src/runGenerator.ts | 2 + .../src/runLocalGenerationForWorkspace.ts | 69 ++++----- .../src/pipeline/github/parseCommitMessage.ts | 14 +- .../src/pipeline/steps/GithubStep.ts | 7 +- packages/generator-cli/src/pipeline/types.ts | 4 + 21 files changed, 636 insertions(+), 64 deletions(-) diff --git a/packages/cli/ai/baml_src/diff_analyzer.baml b/packages/cli/ai/baml_src/diff_analyzer.baml index c854387d1995..1b4f2ead976a 100644 --- a/packages/cli/ai/baml_src/diff_analyzer.baml +++ b/packages/cli/ai/baml_src/diff_analyzer.baml @@ -21,6 +21,9 @@ class AnalyzeCommitDiffResponse { version_bump VersionBump @description("The recommended semantic version bump: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes and other changes, NO_CHANGE for empty diffs") + + version_bump_reason string + @description("One sentence explaining WHY this version bump was chosen. For MAJOR: name the specific breaking symbol(s). For MINOR: name the new capability. For PATCH: describe the fix. For NO_CHANGE: 'No functional changes detected.' Example: 'MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`.'") } // Main function that analyzes SDK diffs @@ -612,9 +615,81 @@ function AnalyzeSdkDiff( - Do not use conventional commit prefixes (no "feat:", "fix:", etc.) - Write in third person ("The SDK now supports..." not "Add support for...") - Remember again that YOU MUST return a structured JSON response with these three fields: + Remember again that YOU MUST return a structured JSON response with these four fields: - message: A git commit message formatted like the example previously provided - changelog_entry: A user-facing release note (empty string for PATCH) - version_bump: One of: MAJOR, MINOR, PATCH, or NO_CHANGE + - version_bump_reason: One sentence explaining WHY this bump level was chosen. + For MAJOR, name the specific breaking symbol(s) and explain why existing callers break. + For MINOR, name the new capability added. + For PATCH, describe what was fixed or improved. + For NO_CHANGE, say "No functional changes detected." + Example: "MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`, breaking existing callers." + "# +} + +class ConsolidateChangelogResponse { + consolidated_changelog string + @description("CHANGELOG.md entry in Keep a Changelog format. Group under ### Breaking Changes, ### Added, ### Changed, ### Fixed. Bold symbol names, one tight sentence per bullet. Prose only, no code fences. Append migration action inline for breaking changes.") + + pr_description string + @description("PR description with ## Breaking Changes section (if any) containing ### per breaking change with Before/After code fences and Migration line, then ## What's New section summarizing features in prose paragraphs grouped by theme. Do NOT list every class individually — summarize repetitive changes as a single entry.") + + version_bump_reason string + @description("One sentence explaining WHY the overall version bump was chosen. For MAJOR: name the specific breaking symbol(s). For MINOR: name the new capability. For PATCH: describe the fix. Example: 'MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`.'") +} + +function ConsolidateChangelog( + raw_entries: string @description("Newline-separated raw changelog entries from chunked diff analysis"), + version_bump: string @description("The overall version bump: MAJOR, MINOR, or PATCH"), + language: string @description("The SDK programming language, e.g. 'typescript', 'python', 'java'") +) -> ConsolidateChangelogResponse { + client DefaultClient + + prompt #" + You are a technical writer formatting release notes for a {{language}} SDK. + + The raw change notes below are noisy and repetitive — many bullets describe the same + change across different packages. Deduplicate aggressively: if the same feature appears + multiple times, merge into one entry. + + Raw changelog entries: + --- + {{raw_entries}} + --- + + Overall version bump: {{version_bump}} + + Produce three outputs: + + --- + + ## 1. CHANGELOG.md entry (Keep a Changelog format) + + - Group under: `### Breaking Changes`, `### Added`, `### Changed`, `### Fixed` + - Only include sections with entries + - **Bold the symbol name** first, then one tight sentence for SDK consumers + - No code fences — prose only + - For breaking changes, append the migration action inline + + ## 2. PR Description + + - `## Breaking Changes` section at top (if any) + - One `###` per breaking change with **Before/After** code fences and a **Migration:** line + - `## What's New` section summarizing added/changed features in prose paragraphs, + grouped by theme (e.g. logging, streaming, pagination, builder improvements) + - Do NOT list every class that got the same method — summarize as a single entry + + ## 3. Version Bump Reason + + - One sentence explaining WHY the overall version bump ({{version_bump}}) was chosen + - For MAJOR: name the specific breaking symbol(s) and explain why existing callers break + - For MINOR: name the new capability added + - For PATCH: describe what was fixed or improved + - Example: "MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`, breaking existing callers." + + --- + + {{ ctx.output_format }} "# } diff --git a/packages/cli/ai/src/baml_client/async_client.ts b/packages/cli/ai/src/baml_client/async_client.ts index dd90a842719f..d92fdfdb968c 100644 --- a/packages/cli/ai/src/baml_client/async_client.ts +++ b/packages/cli/ai/src/baml_client/async_client.ts @@ -24,7 +24,7 @@ import { toBamlError, BamlStream, BamlAbortError, Collector, ClientRegistry } fr import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" import type { partial_types } from "./partial_types" import type * as types from "./types" -import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, VersionBump} from "./types" +import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, ConsolidateChangelogResponse, VersionBump} from "./types" import type TypeBuilder from "./type_builder" import { AsyncHttpRequest, AsyncHttpStreamRequest } from "./async_request" import { LlmResponseParser, LlmStreamParser } from "./parser" @@ -153,6 +153,62 @@ export type RecursivePartialNull = MovedRecursivePartialNull } } + async ConsolidateChangelog( + raw_entries: string,version_bump: string,language: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + // Check if onTick is provided - route through streaming if so + if (__options__.onTick) { + const __stream__ = this.stream.ConsolidateChangelog( + raw_entries,version_bump,language, + __baml_options__ + ); + + return await __stream__.getFinalResponse(); + } + + const __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : + [__options__.collector]) : []; + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __options__.clientRegistry; + if (__options__.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__options__.client); + } + + const __raw__ = await this.runtime.callFunction( + "ConsolidateChangelog", + { + "raw_entries": raw_entries,"version_bump": version_bump,"language": language + }, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __clientRegistry__, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __options__.watchers, + ) + return __raw__.parsed(false) as types.ConsolidateChangelogResponse + } catch (error) { + throw toBamlError(error); + } + } + } class BamlStreamClient { @@ -241,6 +297,80 @@ export type RecursivePartialNull = MovedRecursivePartialNull } } + ConsolidateChangelog( + raw_entries: string,version_bump: string,language: string, + __baml_options__?: BamlCallOptions + ): BamlStream + { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + let __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : + [__options__.collector]) : []; + + let __onTickWrapper__: (() => void) | undefined; + + // Create collector and wrap onTick if provided + if (__options__.onTick) { + const __tickCollector__ = new Collector("on-tick-collector"); + __collector__ = [...__collector__, __tickCollector__]; + + __onTickWrapper__ = () => { + const __log__ = __tickCollector__.last; + if (__log__) { + try { + __options__.onTick!("Unknown", __log__); + } catch (error) { + console.error("Error in onTick callback for ConsolidateChangelog", error); + } + } + }; + } + + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __options__.clientRegistry; + if (__options__.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__options__.client); + } + + const __raw__ = this.runtime.streamFunction( + "ConsolidateChangelog", + { + "raw_entries": raw_entries,"version_bump": version_bump,"language": language + }, + undefined, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __clientRegistry__, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __onTickWrapper__, + ) + return new BamlStream( + __raw__, + (a): partial_types.ConsolidateChangelogResponse => a, + (a): types.ConsolidateChangelogResponse => a, + this.ctxManager.cloneContext(), + __options__.signal, + ) + } catch (error) { + throw toBamlError(error); + } + } + } export const b = new BamlAsyncClient(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME, diff --git a/packages/cli/ai/src/baml_client/async_request.ts b/packages/cli/ai/src/baml_client/async_request.ts index 5a3ff2458b96..f67f8ffa519b 100644 --- a/packages/cli/ai/src/baml_client/async_request.ts +++ b/packages/cli/ai/src/baml_client/async_request.ts @@ -23,7 +23,7 @@ import type { BamlRuntime, BamlCtxManager, Image, Audio, Pdf, Video, FunctionLog import { toBamlError, HTTPRequest, ClientRegistry } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type * as types from "./types" -import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, VersionBump} from "./types" +import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, ConsolidateChangelogResponse, VersionBump} from "./types" import type TypeBuilder from "./type_builder" import type * as events from "./events" @@ -75,6 +75,39 @@ env?: Record } } + async ConsolidateChangelog( + raw_entries: string,version_bump: string,language: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return await this.runtime.buildRequest( + "ConsolidateChangelog", + { + "raw_entries": raw_entries,"version_bump": version_bump,"language": language + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + false, + __env__ + ) + } catch (error) { + throw toBamlError(error); + } + } + } export class AsyncHttpStreamRequest { @@ -114,4 +147,37 @@ env?: Record } } + async ConsolidateChangelog( + raw_entries: string,version_bump: string,language: string, + __baml_options__?: BamlCallOptions + ): Promise { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return await this.runtime.buildRequest( + "ConsolidateChangelog", + { + "raw_entries": raw_entries,"version_bump": version_bump,"language": language + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + true, + __env__ + ) + } catch (error) { + throw toBamlError(error); + } + } + } \ No newline at end of file diff --git a/packages/cli/ai/src/baml_client/inlinedbaml.ts b/packages/cli/ai/src/baml_client/inlinedbaml.ts index 76402ee6e1de..fd83093d6899 100644 --- a/packages/cli/ai/src/baml_client/inlinedbaml.ts +++ b/packages/cli/ai/src/baml_client/inlinedbaml.ts @@ -20,7 +20,7 @@ $ pnpm add @boundaryml/baml const fileMap = { - "diff_analyzer.baml": "// SDK Diff Analyzer\n// Analyzes git diffs of SDK code and produces semantic commit messages and version bumps\n\nenum VersionBump {\n MAJOR\n MINOR\n PATCH\n NO_CHANGE\n}\n\nclass AnalyzeCommitDiffRequest {\n diff string @description(\"The git diff to analyze for generating a commit message\")\n}\n\nclass AnalyzeCommitDiffResponse {\n message string\n @description(\"Developer-facing git commit message. Use conventional commit format. Reference specific code symbols (function names, class names). Keep summary under 72 chars.\")\n\n changelog_entry string\n @description(\"User-facing release note for CHANGELOG.md and GitHub Releases. Describe the impact on SDK consumers, not implementation details. Include migration instructions for MAJOR. Use plain prose, not conventional commit format. Empty string for PATCH.\")\n\n version_bump VersionBump\n @description(\"The recommended semantic version bump: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes and other changes, NO_CHANGE for empty diffs\")\n}\n\n// Main function that analyzes SDK diffs\nfunction AnalyzeSdkDiff(\n diff: string @description(\"The git diff to analyze\"),\n language: string @description(\"The SDK programming language, e.g. 'typescript', 'python', 'java'\"),\n previous_version: string @description(\"The current published version before this change, e.g. '1.2.3'\"),\n prior_changelog: string @description(\"The last 3 changelog entries for this SDK, empty string if none. Use this to match the existing commit message style and understand the versioning pattern.\"),\n spec_commit_message: string @description(\"The commit message from the API spec repository that triggered this SDK generation. Use as a hint for the intent of the change, but verify against the actual diff. Empty string if unavailable.\")\n) -> AnalyzeCommitDiffResponse {\n client DefaultClient\n\n prompt #\"\n You are an expert software engineer analyzing changes to generate semantic commit messages.\n\n Analyze the provided git diff and return a structured response with these fields:\n - message: A git commit message formatted like the example below\n - version_bump: One of: MAJOR, MINOR, PATCH, or NO_CHANGE\n\n Version Bump Guidelines:\n - MAJOR: Breaking changes (removed/renamed functions, changed signatures, removed parameters)\n - MINOR: New features that are backward compatible (new functions, new optional parameters).\n Also MINOR: behavioral changes invisible to the public API surface that still affect consumers:\n - Changed HTTP status code handling (e.g. 404 now throws instead of returning null)\n - Changed default parameter values (timeout, retry count, page size, base URL)\n - Changed serialization behavior (date formats, null handling, field ordering)\n - Changed error message text that consumers may depend on\n - Changed HTTP header names or values sent to the server\n - Changed retry or backoff behavior (different retry counts, delay strategies)\n - PATCH: Bug fixes, documentation, internal refactoring with no observable behavioral change\n - NO_CHANGE: The diff is empty\n\n --- Examples ---\n\n Examples of correct classifications:\n\n --- MAJOR: removed exported TypeScript function ---\n diff --git a/src/api/client.ts b/src/api/client.ts\n -export async function getUser(id: string): Promise {\n - return this.request(\"GET\", `/users/${id}`);\n -}\n version_bump: MAJOR\n reason: Existing callers of getUser() will get a compile error.\n\n --- MAJOR: removed Python public method ---\n diff --git a/vital/client.py b/vital/client.py\n - def get_user(self, user_id: str) -> User:\n - return self._request(\"GET\", f\"/users/{user_id}\")\n version_bump: MAJOR\n reason: Existing callers crash with AttributeError.\n\n --- MINOR: new optional TypeScript parameter ---\n diff --git a/src/api/client.ts b/src/api/client.ts\n -async createUser(name: string): Promise\n +async createUser(name: string, role?: UserRole): Promise\n version_bump: MINOR\n reason: Existing callers unaffected — new parameter is optional.\n\n --- MINOR: new Java public method ---\n diff --git a/src/.../UsersClient.java b/src/.../UsersClient.java\n + public CompletableFuture getUserAsync(String userId) {\n + return this.httpClient.sendAsync(...);\n + }\n version_bump: MINOR\n reason: New capability added, nothing removed or changed.\n\n --- MINOR: changed default retry count ---\n diff --git a/src/core/http_client.py b/src/core/http_client.py\n -MAX_RETRIES = 3\n +MAX_RETRIES = 5\n version_bump: MINOR\n reason: Changed default retry count — existing consumers will experience different retry behavior.\n\n --- PATCH: Go import reorganization ---\n diff --git a/client.go b/client.go\n -import \"fmt\"\n -import \"net/http\"\n +import (\n + \"fmt\"\n + \"net/http\"\n +)\n version_bump: PATCH\n reason: Formatting change only, no functional difference.\n\n --- End Examples ---\n\n {% if language == \"typescript\" %}\n Language-specific breaking change rules for TypeScript:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, function, or exported symbol is MAJOR\n - Making a response field optional is MAJOR (type changes from T to T | undefined, breaking existing property access without null checks)\n - Changing a method return type (e.g. Promise to Promise, or T to T | null) is MAJOR\n - New enum values are MAJOR if the SDK generates exhaustive switch/if-else chains (callers get compile errors on unhandled cases)\n - Changing a union type (adding/removing variants) is MAJOR if callers use exhaustive type narrowing or discriminated unions\n - Adding a new required property to a request/input type is MAJOR (existing callers won't compile)\n - Removing or renaming an exported type, interface, or type alias is MAJOR\n - Changing the type of an existing property (e.g. string to number, or string to string[]) is MAJOR\n - Changing a generic type parameter constraint (e.g. to ) is MAJOR\n - Converting a synchronous method to async (or vice versa) is MAJOR (changes return type to Promise)\n - Removing a default export or switching between default and named exports is MAJOR\n - Changing the structure of a discriminated union (e.g. changing the discriminant field name) is MAJOR\n - Removing or renaming environment/server URL constants is MAJOR\n - Changing the constructor signature of a client class (adding required params) is MAJOR\n - Narrowing a parameter type (e.g. string | number to string) is MAJOR (callers passing number break)\n\n MINOR (backward-compatible additions):\n - Adding a new optional parameter to a function is MINOR\n - Adding new exported types, interfaces, or classes is MINOR\n - Adding new methods to an existing client class is MINOR\n - Adding new optional properties to request types is MINOR\n - Adding new enum values when NOT used in exhaustive checks is MINOR\n - Adding new environment/server URL constants is MINOR\n - Widening a parameter type (e.g. string to string | number) is MINOR\n - Adding new re-exports from index files is MINOR\n - Adding new error types or exception classes is MINOR\n - Adding new RequestOptions fields (e.g. timeout, retries) is MINOR\n - Deprecating (but not removing) a public API is MINOR\n\n PATCH (no API surface change):\n - Changes to internal/private modules (core/, _internal/, utils/) are PATCH\n - Reordering imports, formatting, or comment changes are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) is PATCH\n - Refactoring HTTP client internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in package.json is PATCH\n - Adding or modifying JSDoc/TSDoc comments is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to .npmignore, tsconfig.json, or build configuration are PATCH\n - Updating serialization/deserialization logic that preserves the same public types is PATCH\n\n {% elif language == \"python\" %}\n Language-specific breaking change rules for Python:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, or function is always MAJOR\n - Adding a new required parameter to a public method is MAJOR (callers get TypeError)\n - Renaming a parameter in a public method is MAJOR if callers use keyword arguments\n - Removing a parameter from a public method signature is MAJOR\n - Changing the type of a parameter from one concrete type to an incompatible one is MAJOR\n - Changing exception types raised by a method (callers catching specific exceptions break) is MAJOR\n - Removing or renaming a public module or subpackage is MAJOR (import statements break)\n - Moving a public class/function to a different module without re-exporting from the original is MAJOR\n - Changing a class from inheriting one base to another when callers use isinstance() checks is MAJOR\n - Removing a public class attribute or property is MAJOR\n\n MINOR (backward-compatible additions):\n - Making a response field optional is usually MINOR (Python uses None propagation; callers rarely type-check strictly)\n - New enum values are MINOR (unknown values are handled gracefully with string fallbacks)\n - Changing a return type from a concrete type to Optional is MINOR (duck typing absorbs this)\n - Adding new public methods, classes, or modules is MINOR\n - Adding new optional parameters (with defaults) is MINOR\n - Adding new optional fields to Pydantic models is MINOR\n - Adding new exception/error classes is MINOR\n - Adding new class attributes or properties is MINOR\n - Adding new type overloads (@overload decorator) is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. timeout, max_retries) is MINOR\n - Deprecating (but not removing) a public API is MINOR\n - Widening a type annotation (e.g. str to Union[str, int]) is MINOR\n\n PATCH (no API surface change):\n - Changes to private methods (prefixed with _) are PATCH\n - Changes to type hints only (no runtime effect) are PATCH\n - Reformatting, docstring updates, or comment changes are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) are PATCH\n - Refactoring httpx/requests client internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in pyproject.toml/setup.py is PATCH\n - Updating serialization/deserialization logic that preserves the same public types is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to __init__.py that don't alter public re-exports are PATCH\n - Changes to conftest.py, test files, or CI configuration are PATCH\n\n {% elif language == \"java\" %}\n Language-specific breaking change rules for Java:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, or interface is always MAJOR\n - Making a response field optional (e.g. T to Optional) is MAJOR (callers must handle Optional unwrapping)\n - New enum values are MAJOR if the SDK generates exhaustive switch statements\n - Adding a new required parameter to a public method is MAJOR\n - Changing a method's return type is MAJOR (even if compatible at runtime, recompilation fails)\n - Removing or changing a public static final constant is MAJOR\n - Changing a class from concrete to abstract (or vice versa) is MAJOR\n - Changing the checked exceptions declared in a throws clause is MAJOR\n - Removing a public constructor or changing its parameter list is MAJOR\n - Removing an interface that a public class implements is MAJOR\n - Changing generic type parameters on a public class (e.g. Foo to Foo) is MAJOR\n - Moving a public class to a different package without re-exporting is MAJOR\n - Narrowing a parameter type (e.g. Object to String) is MAJOR\n - Making a non-final class final is MAJOR (breaks subclassing)\n - Changing the type of a builder method parameter is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new overloaded methods is MINOR\n - Adding new public classes or interfaces is MINOR\n - Adding new optional builder methods is MINOR\n - Adding new enum values when NOT used in exhaustive switch statements is MINOR\n - Adding new optional fields to request objects is MINOR\n - Adding new exception/error classes is MINOR\n - Adding new static utility methods is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. timeout, retries) is MINOR\n - Widening a parameter type (e.g. String to Object) is MINOR\n - Adding default methods to interfaces (Java 8+) is MINOR\n - Deprecating (but not removing) public APIs with @Deprecated is MINOR\n\n PATCH (no API surface change):\n - Changes to package-private or private methods are PATCH\n - Changes to annotations (other than public API annotations), Javadoc, or formatting are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) is PATCH\n - Refactoring OkHttp/HttpClient internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in build.gradle/pom.xml is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization logic (Jackson/Gson config) that preserves public types is PATCH\n - Changes to test files, CI configuration, or build scripts are PATCH\n\n {% elif language == \"go\" %}\n Language-specific breaking change rules for Go:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming an exported function, method, or type is always MAJOR\n - Making a response field a pointer type (e.g. string to *string) is MAJOR (callers must dereference)\n - Changing a function signature (adding/removing parameters or return values) is MAJOR (Go has no overloading)\n - Removing or renaming an exported struct field is MAJOR\n - Changing the type of an exported struct field is MAJOR\n - Removing or renaming an exported constant or variable is MAJOR\n - Changing an interface by adding methods is MAJOR (all implementations must add the method)\n - Removing a method from an interface is MAJOR (callers using that method break)\n - Changing a function's return type(s) is MAJOR (Go is strict about return types)\n - Changing a variadic parameter to non-variadic (or vice versa) is MAJOR\n - Moving a type/function to a different package without aliasing in the original is MAJOR\n - Changing the receiver type of a method (value receiver to pointer receiver changes method set) is MAJOR\n - Changing an exported error variable's type or value is MAJOR (callers using errors.Is break)\n\n MINOR (backward-compatible additions):\n - Adding new exported functions, methods, or types is MINOR\n - New enum-like constants are MINOR (Go enums are not exhaustive by default)\n - Adding new fields to a struct is MINOR (existing code still compiles, zero-value initialization)\n - Making a response field optional (pointer) is usually MINOR if the field was already a struct field\n - Adding new optional function parameters via functional options pattern is MINOR\n - Adding new interface types is MINOR\n - Adding new error types/variables is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOption functions is MINOR\n - Widening a return type from a concrete type to an interface is MINOR (if interface is satisfied)\n - Adding new methods to a concrete type (does not break interface implementations) is MINOR\n\n PATCH (no API surface change):\n - Changes to unexported (lowercase) functions or types are PATCH\n - Changes to go.mod dependencies, import reordering, or formatting are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) is PATCH\n - Refactoring http.Client internals without changing observable defaults or behavior is PATCH\n - Updating serialization/deserialization logic (JSON tags, encoding) that preserves identical output is PATCH\n - Refactoring internal implementation without changing exported signatures is PATCH\n - Changes to *_test.go files, Makefile, or CI configuration are PATCH\n - Updating comments, godoc, or code formatting (gofmt) is PATCH\n\n {% elif language == \"ruby\" %}\n Language-specific breaking change rules for Ruby:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public method is MAJOR (callers get NoMethodError)\n - Adding a new required positional parameter is MAJOR\n - Removing a method parameter is MAJOR (callers passing that argument get ArgumentError)\n - Changing the order of positional parameters is MAJOR\n - Removing or renaming a public class or module is MAJOR (callers get NameError)\n - Changing a method from instance to class method (or vice versa) is MAJOR\n - Changing the return type in a breaking way (e.g. returning nil where an object was expected and callers chain methods) is MAJOR\n - Removing a public constant is MAJOR\n - Changing exception types raised by a method is MAJOR (callers rescuing specific exceptions break)\n\n MINOR (backward-compatible additions):\n - Making a response field optional is usually MINOR (Ruby uses nil propagation; callers rarely type-check)\n - New enum values are MINOR (unknown values are handled gracefully)\n - Adding new optional keyword parameters (with defaults) is MINOR\n - Adding new public methods or classes is MINOR\n - Adding new optional fields to response/request objects is MINOR\n - Adding new exception/error classes is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. timeout, max_retries) is MINOR\n - Deprecating (but not removing) a public API is MINOR\n - Adding new modules or mixins is MINOR\n\n PATCH (no API surface change):\n - Changes to private methods are PATCH\n - Gemspec metadata, comment, or formatting changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring Faraday/Net::HTTP internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in Gemfile/gemspec is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to test files, Rakefile, or CI configuration are PATCH\n\n {% elif language == \"csharp\" %}\n Language-specific breaking change rules for C#:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, property, or interface is always MAJOR\n - Making a response field nullable (e.g. T to T?) is MAJOR (callers must handle null checks)\n - New enum values are MAJOR if the SDK generates exhaustive switch expressions\n - Adding a new required parameter to a public method is MAJOR\n - Changing a method's return type is MAJOR\n - Removing or changing a public constant or static readonly field is MAJOR\n - Changing a class from non-sealed to sealed (or abstract to concrete) is MAJOR\n - Changing the base class of a public class is MAJOR\n - Removing an interface implementation from a public class is MAJOR\n - Changing generic type constraints on a public class or method is MAJOR\n - Moving a public type to a different namespace without type forwarding is MAJOR\n - Changing property from read-write to read-only (removing setter) is MAJOR\n - Changing async method to sync (Task to T) or vice versa is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public classes, interfaces, or methods is MINOR\n - Adding new optional parameters (with default values) is MINOR\n - Adding new overloaded methods is MINOR\n - Adding new enum values when NOT used in exhaustive switch expressions is MINOR\n - Adding new optional properties to request objects is MINOR\n - Adding new exception types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. Timeout, MaxRetries) is MINOR\n - Adding new extension methods is MINOR\n - Deprecating (but not removing) public APIs with [Obsolete] is MINOR\n\n PATCH (no API surface change):\n - Changes to internal or private members are PATCH\n - XML doc comments, formatting, or namespace reorganization are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring HttpClient internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in .csproj is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization (System.Text.Json/Newtonsoft) config that preserves public types is PATCH\n - Changes to test files, .sln, or CI configuration are PATCH\n\n {% elif language == \"php\" %}\n Language-specific breaking change rules for PHP:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public method or class is always MAJOR\n - Changing a method signature (adding required parameters) is MAJOR\n - Removing a method parameter is MAJOR\n - Changing the type declaration of a parameter to an incompatible type is MAJOR\n - Removing or renaming a public constant is MAJOR\n - Changing a class from non-final to final is MAJOR (breaks extension)\n - Removing an interface implementation from a public class is MAJOR\n - Changing the return type declaration to an incompatible type is MAJOR\n - Moving a class to a different namespace without aliasing is MAJOR\n - Changing exception types thrown by a method is MAJOR\n\n MINOR (backward-compatible additions):\n - Making a response field nullable is MINOR in most cases (PHP is dynamically typed)\n - Adding new optional parameters (with defaults) is MINOR\n - Adding new public classes or methods is MINOR\n - New enum cases are usually MINOR (PHP enums are not typically used in exhaustive matches)\n - Adding new optional fields to request/response objects is MINOR\n - Adding new exception classes is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new traits or interfaces is MINOR\n - Adding new RequestOptions fields is MINOR\n - Deprecating (but not removing) public APIs is MINOR\n\n PATCH (no API surface change):\n - Changes to private/protected methods are PATCH\n - PHPDoc, formatting, or composer.json metadata changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring Guzzle/cURL internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in composer.json is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to test files, phpunit.xml, or CI configuration are PATCH\n\n {% elif language == \"swift\" %}\n Language-specific breaking change rules for Swift:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public method, property, or type is always MAJOR\n - Making a response field optional (T to T?) is MAJOR (callers must unwrap with if-let/guard)\n - New enum cases are MAJOR (Swift switch statements must be exhaustive unless using default)\n - Adding a new required parameter to a public method is MAJOR\n - Changing the type of a public property is MAJOR\n - Removing or changing a public protocol requirement is MAJOR\n - Removing protocol conformance from a public type is MAJOR\n - Changing a struct to a class (or vice versa) is MAJOR (value vs reference semantics)\n - Making a public initializer failable (init to init?) or vice versa is MAJOR\n - Changing the associated values of an enum case is MAJOR\n - Removing a public typealias is MAJOR\n - Changing access level from public to internal/private is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public types, methods, or properties is MINOR\n - Adding new optional parameters (with default values) is MINOR\n - Adding new enum cases when callers use default in switch is MINOR\n - Adding new protocol extensions with default implementations is MINOR\n - Adding new optional fields to request/response structs is MINOR\n - Adding new error types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields is MINOR\n - Adding new convenience initializers is MINOR\n - Deprecating (but not removing) public APIs with @available(*, deprecated) is MINOR\n\n PATCH (no API surface change):\n - Changes to internal or private members are PATCH\n - Formatting, comments, or documentation changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring URLSession internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in Package.swift is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to test files, xcconfig, or CI configuration are PATCH\n\n {% elif language == \"rust\" %}\n Language-specific breaking change rules for Rust:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public function, struct, enum, or trait is always MAJOR\n - Making a response field optional (T to Option) is MAJOR (callers must handle the Option)\n - New enum variants are MAJOR (Rust match statements must be exhaustive unless using _ wildcard)\n - Adding a new required field to a public struct is MAJOR (unless #[non_exhaustive])\n - Removing a public trait implementation is MAJOR\n - Changing a function's return type is MAJOR\n - Adding a required method to a public trait is MAJOR (all implementations must add it)\n - Changing the type of a public struct field is MAJOR\n - Removing or renaming a public module is MAJOR\n - Making a public type private (pub to pub(crate) or removing pub) is MAJOR\n - Changing a struct from non-exhaustive to exhaustive construction (removing .. Default::default()) is MAJOR\n - Changing generic type parameters or their bounds on public types is MAJOR\n - Changing from Result to Result where E2 is a different error type is MAJOR\n - Removing a public constant or static is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public functions, structs, or enums is MINOR\n - Adding new optional fields to #[non_exhaustive] structs is MINOR\n - Adding new enum variants to #[non_exhaustive] enums is MINOR\n - Adding new trait implementations for existing types is MINOR\n - Adding new public constants or statics is MINOR\n - Adding new methods to existing impl blocks is MINOR\n - Adding new error types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields is MINOR\n - Adding new optional builder methods is MINOR\n - Deprecating (but not removing) public APIs with #[deprecated] is MINOR\n\n PATCH (no API surface change):\n - Changes to pub(crate) or private items are PATCH\n - Cargo.toml metadata, formatting, or comment changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring reqwest/hyper internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in Cargo.toml is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization (serde) config that preserves public types is PATCH\n - Changes to test files, build.rs, or CI configuration are PATCH\n\n {% elif language == \"kotlin\" %}\n Language-specific breaking change rules for Kotlin:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public function, class, or property is always MAJOR\n - Making a response field nullable (T to T?) is MAJOR (callers must handle null safety operators)\n - New enum values are MAJOR if used in when() expressions without else branch\n - Adding a new required parameter to a public function is MAJOR\n - Changing a method's return type is MAJOR\n - Removing or changing a public constant (const val / companion object val) is MAJOR\n - Changing a class from open to final (or data class to regular class) is MAJOR\n - Removing an interface implementation from a public class is MAJOR\n - Changing generic type parameters or their variance (in/out) on public types is MAJOR\n - Moving a public class to a different package without type aliasing is MAJOR\n - Changing a property from var to val (or removing a setter) is MAJOR\n - Changing a suspend function to non-suspend (or vice versa) is MAJOR\n - Changing sealed class/interface hierarchy (removing subclasses) is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public classes, functions, or extension functions is MINOR\n - Adding new optional parameters (with default values) is MINOR\n - Adding new enum values when callers use else in when() is MINOR\n - Adding new optional properties to data classes is MINOR\n - Adding new exception types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields is MINOR\n - Adding new sealed class/interface subtypes is MINOR (if callers have else branch)\n - Deprecating (but not removing) public APIs with @Deprecated is MINOR\n - Adding new companion object functions is MINOR\n\n PATCH (no API surface change):\n - Changes to internal or private members are PATCH\n - KDoc, formatting, or build.gradle changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring OkHttp internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in build.gradle.kts is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization (kotlinx.serialization/Moshi) config that preserves public types is PATCH\n - Changes to test files or CI configuration are PATCH\n\n {% else %}\n Language-specific breaking change rules (language: {{ language }}):\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, or function is always MAJOR\n - Making a response field optional is MAJOR in statically-typed languages (TypeScript, Java, C#, Swift, Rust, Kotlin, Go), usually MINOR in dynamically-typed ones (Python, Ruby, PHP)\n - New enum values are MAJOR if the language enforces exhaustive matching (TypeScript, Java, C#, Swift, Rust), MINOR otherwise\n - Adding a new required parameter to a public method is MAJOR\n - Changing a method's return type is MAJOR\n - Changing the type of an existing field/property is MAJOR\n - Removing or changing public constants is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public APIs (classes, methods, functions) is MINOR\n - Adding new optional parameters is MINOR\n - Adding new optional fields to request/response objects is MINOR\n - Adding new error/exception types is MINOR\n - Deprecating (but not removing) public APIs is MINOR\n\n PATCH (no API surface change):\n - Internal/private changes are PATCH\n - Formatting, documentation, or comment changes are PATCH\n - Dependency version updates are PATCH\n - SDK version header updates are PATCH\n - Refactoring retry/timeout internals without changing observable defaults or behavior is PATCH\n - Refactoring internals without changing public signatures is PATCH\n {% endif %}\n\n Apply these patterns to the diff below. When in doubt between MINOR and PATCH,\n prefer MINOR. When in doubt between MAJOR and MINOR, examine whether existing\n callers would break without any code changes on their side.\n\n Message Format (use this exact structure):\n ```\n : \n\n \n\n Key changes:\n - \n - \n - \n ```\n\n Message Guidelines:\n - Use conventional commit types: feat, fix, refactor, docs, chore, test, style, perf\n - Keep the summary line under 72 characters\n - Write in present tense imperative mood (\"add\" not \"added\" or \"adds\")\n - For breaking changes: include migration instructions in the detailed description\n - For new features: highlight new capabilities in the key changes\n - For PATCH: describe the fix or improvement\n - For NO_CHANGE: use type \"chore\" and state that no functional changes were made\n - Be specific and action-oriented\n - Do not use \"Fern regeneration\" in commit messages - use \"SDK regeneration\" instead\n - NEVER include the literal version \"505.503.4455\" in the commit message - if you see this placeholder\n in the diff, describe changes generically (e.g., \"added X-Fern-SDK-Version header\")\n - The previous version is provided for context only. Do not include it\n literally in the commit message summary line.\n\n {% if prior_changelog %}\n Prior changelog entries (for style reference):\n ---\n {{prior_changelog}}\n ---\n Match the tone and format of these entries in your commit message.\n {% endif %}\n\n {% if spec_commit_message %}\n The API spec change that triggered this SDK generation had the following commit message:\n \"{{spec_commit_message}}\"\n Use this as a hint for understanding the intent of the change, but always verify\n against the actual diff below. The commit message may be vague or inaccurate.\n {% endif %}\n\n Previous version: {{previous_version}}\n SDK language: {{language}}\n\n Git Diff:\n ---\n {{diff}}\n ---\n\n Changelog Entry Guidelines:\n - Write for SDK consumers, not engineers reading the source code\n - MAJOR: explain what broke and how to migrate (\"The `getUser` method has been\n removed. Replace calls with `fetchUser(id)` which returns the same type.\")\n - MINOR: describe the new capability (\"New `createPayment()` method available\n on `PaymentsClient`.\")\n - PATCH: leave empty string — patch changes don't warrant changelog entries\n - Do not use conventional commit prefixes (no \"feat:\", \"fix:\", etc.)\n - Write in third person (\"The SDK now supports...\" not \"Add support for...\")\n\n Remember again that YOU MUST return a structured JSON response with these three fields:\n - message: A git commit message formatted like the example previously provided\n - changelog_entry: A user-facing release note (empty string for PATCH)\n - version_bump: One of: MAJOR, MINOR, PATCH, or NO_CHANGE\n \"#\n}\n", + "diff_analyzer.baml": "// SDK Diff Analyzer\n// Analyzes git diffs of SDK code and produces semantic commit messages and version bumps\n\nenum VersionBump {\n MAJOR\n MINOR\n PATCH\n NO_CHANGE\n}\n\nclass AnalyzeCommitDiffRequest {\n diff string @description(\"The git diff to analyze for generating a commit message\")\n}\n\nclass AnalyzeCommitDiffResponse {\n message string\n @description(\"Developer-facing git commit message. Use conventional commit format. Reference specific code symbols (function names, class names). Keep summary under 72 chars.\")\n\n changelog_entry string\n @description(\"User-facing release note for CHANGELOG.md and GitHub Releases. Describe the impact on SDK consumers, not implementation details. Include migration instructions for MAJOR. Use plain prose, not conventional commit format. Empty string for PATCH.\")\n\n version_bump VersionBump\n @description(\"The recommended semantic version bump: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes and other changes, NO_CHANGE for empty diffs\")\n\n version_bump_reason string\n @description(\"One sentence explaining WHY this version bump was chosen. For MAJOR: name the specific breaking symbol(s). For MINOR: name the new capability. For PATCH: describe the fix. For NO_CHANGE: 'No functional changes detected.' Example: 'MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`.'\")\n}\n\n// Main function that analyzes SDK diffs\nfunction AnalyzeSdkDiff(\n diff: string @description(\"The git diff to analyze\"),\n language: string @description(\"The SDK programming language, e.g. 'typescript', 'python', 'java'\"),\n previous_version: string @description(\"The current published version before this change, e.g. '1.2.3'\"),\n prior_changelog: string @description(\"The last 3 changelog entries for this SDK, empty string if none. Use this to match the existing commit message style and understand the versioning pattern.\"),\n spec_commit_message: string @description(\"The commit message from the API spec repository that triggered this SDK generation. Use as a hint for the intent of the change, but verify against the actual diff. Empty string if unavailable.\")\n) -> AnalyzeCommitDiffResponse {\n client DefaultClient\n\n prompt #\"\n You are an expert software engineer analyzing changes to generate semantic commit messages.\n\n Analyze the provided git diff and return a structured response with these fields:\n - message: A git commit message formatted like the example below\n - version_bump: One of: MAJOR, MINOR, PATCH, or NO_CHANGE\n\n Version Bump Guidelines:\n - MAJOR: Breaking changes (removed/renamed functions, changed signatures, removed parameters)\n - MINOR: New features that are backward compatible (new functions, new optional parameters).\n Also MINOR: behavioral changes invisible to the public API surface that still affect consumers:\n - Changed HTTP status code handling (e.g. 404 now throws instead of returning null)\n - Changed default parameter values (timeout, retry count, page size, base URL)\n - Changed serialization behavior (date formats, null handling, field ordering)\n - Changed error message text that consumers may depend on\n - Changed HTTP header names or values sent to the server\n - Changed retry or backoff behavior (different retry counts, delay strategies)\n - PATCH: Bug fixes, documentation, internal refactoring with no observable behavioral change\n - NO_CHANGE: The diff is empty\n\n --- Examples ---\n\n Examples of correct classifications:\n\n --- MAJOR: removed exported TypeScript function ---\n diff --git a/src/api/client.ts b/src/api/client.ts\n -export async function getUser(id: string): Promise {\n - return this.request(\"GET\", `/users/${id}`);\n -}\n version_bump: MAJOR\n reason: Existing callers of getUser() will get a compile error.\n\n --- MAJOR: removed Python public method ---\n diff --git a/vital/client.py b/vital/client.py\n - def get_user(self, user_id: str) -> User:\n - return self._request(\"GET\", f\"/users/{user_id}\")\n version_bump: MAJOR\n reason: Existing callers crash with AttributeError.\n\n --- MINOR: new optional TypeScript parameter ---\n diff --git a/src/api/client.ts b/src/api/client.ts\n -async createUser(name: string): Promise\n +async createUser(name: string, role?: UserRole): Promise\n version_bump: MINOR\n reason: Existing callers unaffected — new parameter is optional.\n\n --- MINOR: new Java public method ---\n diff --git a/src/.../UsersClient.java b/src/.../UsersClient.java\n + public CompletableFuture getUserAsync(String userId) {\n + return this.httpClient.sendAsync(...);\n + }\n version_bump: MINOR\n reason: New capability added, nothing removed or changed.\n\n --- MINOR: changed default retry count ---\n diff --git a/src/core/http_client.py b/src/core/http_client.py\n -MAX_RETRIES = 3\n +MAX_RETRIES = 5\n version_bump: MINOR\n reason: Changed default retry count — existing consumers will experience different retry behavior.\n\n --- PATCH: Go import reorganization ---\n diff --git a/client.go b/client.go\n -import \"fmt\"\n -import \"net/http\"\n +import (\n + \"fmt\"\n + \"net/http\"\n +)\n version_bump: PATCH\n reason: Formatting change only, no functional difference.\n\n --- End Examples ---\n\n {% if language == \"typescript\" %}\n Language-specific breaking change rules for TypeScript:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, function, or exported symbol is MAJOR\n - Making a response field optional is MAJOR (type changes from T to T | undefined, breaking existing property access without null checks)\n - Changing a method return type (e.g. Promise to Promise, or T to T | null) is MAJOR\n - New enum values are MAJOR if the SDK generates exhaustive switch/if-else chains (callers get compile errors on unhandled cases)\n - Changing a union type (adding/removing variants) is MAJOR if callers use exhaustive type narrowing or discriminated unions\n - Adding a new required property to a request/input type is MAJOR (existing callers won't compile)\n - Removing or renaming an exported type, interface, or type alias is MAJOR\n - Changing the type of an existing property (e.g. string to number, or string to string[]) is MAJOR\n - Changing a generic type parameter constraint (e.g. to ) is MAJOR\n - Converting a synchronous method to async (or vice versa) is MAJOR (changes return type to Promise)\n - Removing a default export or switching between default and named exports is MAJOR\n - Changing the structure of a discriminated union (e.g. changing the discriminant field name) is MAJOR\n - Removing or renaming environment/server URL constants is MAJOR\n - Changing the constructor signature of a client class (adding required params) is MAJOR\n - Narrowing a parameter type (e.g. string | number to string) is MAJOR (callers passing number break)\n\n MINOR (backward-compatible additions):\n - Adding a new optional parameter to a function is MINOR\n - Adding new exported types, interfaces, or classes is MINOR\n - Adding new methods to an existing client class is MINOR\n - Adding new optional properties to request types is MINOR\n - Adding new enum values when NOT used in exhaustive checks is MINOR\n - Adding new environment/server URL constants is MINOR\n - Widening a parameter type (e.g. string to string | number) is MINOR\n - Adding new re-exports from index files is MINOR\n - Adding new error types or exception classes is MINOR\n - Adding new RequestOptions fields (e.g. timeout, retries) is MINOR\n - Deprecating (but not removing) a public API is MINOR\n\n PATCH (no API surface change):\n - Changes to internal/private modules (core/, _internal/, utils/) are PATCH\n - Reordering imports, formatting, or comment changes are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) is PATCH\n - Refactoring HTTP client internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in package.json is PATCH\n - Adding or modifying JSDoc/TSDoc comments is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to .npmignore, tsconfig.json, or build configuration are PATCH\n - Updating serialization/deserialization logic that preserves the same public types is PATCH\n\n {% elif language == \"python\" %}\n Language-specific breaking change rules for Python:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, or function is always MAJOR\n - Adding a new required parameter to a public method is MAJOR (callers get TypeError)\n - Renaming a parameter in a public method is MAJOR if callers use keyword arguments\n - Removing a parameter from a public method signature is MAJOR\n - Changing the type of a parameter from one concrete type to an incompatible one is MAJOR\n - Changing exception types raised by a method (callers catching specific exceptions break) is MAJOR\n - Removing or renaming a public module or subpackage is MAJOR (import statements break)\n - Moving a public class/function to a different module without re-exporting from the original is MAJOR\n - Changing a class from inheriting one base to another when callers use isinstance() checks is MAJOR\n - Removing a public class attribute or property is MAJOR\n\n MINOR (backward-compatible additions):\n - Making a response field optional is usually MINOR (Python uses None propagation; callers rarely type-check strictly)\n - New enum values are MINOR (unknown values are handled gracefully with string fallbacks)\n - Changing a return type from a concrete type to Optional is MINOR (duck typing absorbs this)\n - Adding new public methods, classes, or modules is MINOR\n - Adding new optional parameters (with defaults) is MINOR\n - Adding new optional fields to Pydantic models is MINOR\n - Adding new exception/error classes is MINOR\n - Adding new class attributes or properties is MINOR\n - Adding new type overloads (@overload decorator) is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. timeout, max_retries) is MINOR\n - Deprecating (but not removing) a public API is MINOR\n - Widening a type annotation (e.g. str to Union[str, int]) is MINOR\n\n PATCH (no API surface change):\n - Changes to private methods (prefixed with _) are PATCH\n - Changes to type hints only (no runtime effect) are PATCH\n - Reformatting, docstring updates, or comment changes are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) are PATCH\n - Refactoring httpx/requests client internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in pyproject.toml/setup.py is PATCH\n - Updating serialization/deserialization logic that preserves the same public types is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to __init__.py that don't alter public re-exports are PATCH\n - Changes to conftest.py, test files, or CI configuration are PATCH\n\n {% elif language == \"java\" %}\n Language-specific breaking change rules for Java:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, or interface is always MAJOR\n - Making a response field optional (e.g. T to Optional) is MAJOR (callers must handle Optional unwrapping)\n - New enum values are MAJOR if the SDK generates exhaustive switch statements\n - Adding a new required parameter to a public method is MAJOR\n - Changing a method's return type is MAJOR (even if compatible at runtime, recompilation fails)\n - Removing or changing a public static final constant is MAJOR\n - Changing a class from concrete to abstract (or vice versa) is MAJOR\n - Changing the checked exceptions declared in a throws clause is MAJOR\n - Removing a public constructor or changing its parameter list is MAJOR\n - Removing an interface that a public class implements is MAJOR\n - Changing generic type parameters on a public class (e.g. Foo to Foo) is MAJOR\n - Moving a public class to a different package without re-exporting is MAJOR\n - Narrowing a parameter type (e.g. Object to String) is MAJOR\n - Making a non-final class final is MAJOR (breaks subclassing)\n - Changing the type of a builder method parameter is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new overloaded methods is MINOR\n - Adding new public classes or interfaces is MINOR\n - Adding new optional builder methods is MINOR\n - Adding new enum values when NOT used in exhaustive switch statements is MINOR\n - Adding new optional fields to request objects is MINOR\n - Adding new exception/error classes is MINOR\n - Adding new static utility methods is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. timeout, retries) is MINOR\n - Widening a parameter type (e.g. String to Object) is MINOR\n - Adding default methods to interfaces (Java 8+) is MINOR\n - Deprecating (but not removing) public APIs with @Deprecated is MINOR\n\n PATCH (no API surface change):\n - Changes to package-private or private methods are PATCH\n - Changes to annotations (other than public API annotations), Javadoc, or formatting are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) is PATCH\n - Refactoring OkHttp/HttpClient internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in build.gradle/pom.xml is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization logic (Jackson/Gson config) that preserves public types is PATCH\n - Changes to test files, CI configuration, or build scripts are PATCH\n\n {% elif language == \"go\" %}\n Language-specific breaking change rules for Go:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming an exported function, method, or type is always MAJOR\n - Making a response field a pointer type (e.g. string to *string) is MAJOR (callers must dereference)\n - Changing a function signature (adding/removing parameters or return values) is MAJOR (Go has no overloading)\n - Removing or renaming an exported struct field is MAJOR\n - Changing the type of an exported struct field is MAJOR\n - Removing or renaming an exported constant or variable is MAJOR\n - Changing an interface by adding methods is MAJOR (all implementations must add the method)\n - Removing a method from an interface is MAJOR (callers using that method break)\n - Changing a function's return type(s) is MAJOR (Go is strict about return types)\n - Changing a variadic parameter to non-variadic (or vice versa) is MAJOR\n - Moving a type/function to a different package without aliasing in the original is MAJOR\n - Changing the receiver type of a method (value receiver to pointer receiver changes method set) is MAJOR\n - Changing an exported error variable's type or value is MAJOR (callers using errors.Is break)\n\n MINOR (backward-compatible additions):\n - Adding new exported functions, methods, or types is MINOR\n - New enum-like constants are MINOR (Go enums are not exhaustive by default)\n - Adding new fields to a struct is MINOR (existing code still compiles, zero-value initialization)\n - Making a response field optional (pointer) is usually MINOR if the field was already a struct field\n - Adding new optional function parameters via functional options pattern is MINOR\n - Adding new interface types is MINOR\n - Adding new error types/variables is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOption functions is MINOR\n - Widening a return type from a concrete type to an interface is MINOR (if interface is satisfied)\n - Adding new methods to a concrete type (does not break interface implementations) is MINOR\n\n PATCH (no API surface change):\n - Changes to unexported (lowercase) functions or types are PATCH\n - Changes to go.mod dependencies, import reordering, or formatting are PATCH\n - Updating SDK version headers (X-Fern-SDK-Version, User-Agent) is PATCH\n - Refactoring http.Client internals without changing observable defaults or behavior is PATCH\n - Updating serialization/deserialization logic (JSON tags, encoding) that preserves identical output is PATCH\n - Refactoring internal implementation without changing exported signatures is PATCH\n - Changes to *_test.go files, Makefile, or CI configuration are PATCH\n - Updating comments, godoc, or code formatting (gofmt) is PATCH\n\n {% elif language == \"ruby\" %}\n Language-specific breaking change rules for Ruby:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public method is MAJOR (callers get NoMethodError)\n - Adding a new required positional parameter is MAJOR\n - Removing a method parameter is MAJOR (callers passing that argument get ArgumentError)\n - Changing the order of positional parameters is MAJOR\n - Removing or renaming a public class or module is MAJOR (callers get NameError)\n - Changing a method from instance to class method (or vice versa) is MAJOR\n - Changing the return type in a breaking way (e.g. returning nil where an object was expected and callers chain methods) is MAJOR\n - Removing a public constant is MAJOR\n - Changing exception types raised by a method is MAJOR (callers rescuing specific exceptions break)\n\n MINOR (backward-compatible additions):\n - Making a response field optional is usually MINOR (Ruby uses nil propagation; callers rarely type-check)\n - New enum values are MINOR (unknown values are handled gracefully)\n - Adding new optional keyword parameters (with defaults) is MINOR\n - Adding new public methods or classes is MINOR\n - Adding new optional fields to response/request objects is MINOR\n - Adding new exception/error classes is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. timeout, max_retries) is MINOR\n - Deprecating (but not removing) a public API is MINOR\n - Adding new modules or mixins is MINOR\n\n PATCH (no API surface change):\n - Changes to private methods are PATCH\n - Gemspec metadata, comment, or formatting changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring Faraday/Net::HTTP internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in Gemfile/gemspec is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to test files, Rakefile, or CI configuration are PATCH\n\n {% elif language == \"csharp\" %}\n Language-specific breaking change rules for C#:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, property, or interface is always MAJOR\n - Making a response field nullable (e.g. T to T?) is MAJOR (callers must handle null checks)\n - New enum values are MAJOR if the SDK generates exhaustive switch expressions\n - Adding a new required parameter to a public method is MAJOR\n - Changing a method's return type is MAJOR\n - Removing or changing a public constant or static readonly field is MAJOR\n - Changing a class from non-sealed to sealed (or abstract to concrete) is MAJOR\n - Changing the base class of a public class is MAJOR\n - Removing an interface implementation from a public class is MAJOR\n - Changing generic type constraints on a public class or method is MAJOR\n - Moving a public type to a different namespace without type forwarding is MAJOR\n - Changing property from read-write to read-only (removing setter) is MAJOR\n - Changing async method to sync (Task to T) or vice versa is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public classes, interfaces, or methods is MINOR\n - Adding new optional parameters (with default values) is MINOR\n - Adding new overloaded methods is MINOR\n - Adding new enum values when NOT used in exhaustive switch expressions is MINOR\n - Adding new optional properties to request objects is MINOR\n - Adding new exception types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields (e.g. Timeout, MaxRetries) is MINOR\n - Adding new extension methods is MINOR\n - Deprecating (but not removing) public APIs with [Obsolete] is MINOR\n\n PATCH (no API surface change):\n - Changes to internal or private members are PATCH\n - XML doc comments, formatting, or namespace reorganization are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring HttpClient internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in .csproj is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization (System.Text.Json/Newtonsoft) config that preserves public types is PATCH\n - Changes to test files, .sln, or CI configuration are PATCH\n\n {% elif language == \"php\" %}\n Language-specific breaking change rules for PHP:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public method or class is always MAJOR\n - Changing a method signature (adding required parameters) is MAJOR\n - Removing a method parameter is MAJOR\n - Changing the type declaration of a parameter to an incompatible type is MAJOR\n - Removing or renaming a public constant is MAJOR\n - Changing a class from non-final to final is MAJOR (breaks extension)\n - Removing an interface implementation from a public class is MAJOR\n - Changing the return type declaration to an incompatible type is MAJOR\n - Moving a class to a different namespace without aliasing is MAJOR\n - Changing exception types thrown by a method is MAJOR\n\n MINOR (backward-compatible additions):\n - Making a response field nullable is MINOR in most cases (PHP is dynamically typed)\n - Adding new optional parameters (with defaults) is MINOR\n - Adding new public classes or methods is MINOR\n - New enum cases are usually MINOR (PHP enums are not typically used in exhaustive matches)\n - Adding new optional fields to request/response objects is MINOR\n - Adding new exception classes is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new traits or interfaces is MINOR\n - Adding new RequestOptions fields is MINOR\n - Deprecating (but not removing) public APIs is MINOR\n\n PATCH (no API surface change):\n - Changes to private/protected methods are PATCH\n - PHPDoc, formatting, or composer.json metadata changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring Guzzle/cURL internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in composer.json is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to test files, phpunit.xml, or CI configuration are PATCH\n\n {% elif language == \"swift\" %}\n Language-specific breaking change rules for Swift:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public method, property, or type is always MAJOR\n - Making a response field optional (T to T?) is MAJOR (callers must unwrap with if-let/guard)\n - New enum cases are MAJOR (Swift switch statements must be exhaustive unless using default)\n - Adding a new required parameter to a public method is MAJOR\n - Changing the type of a public property is MAJOR\n - Removing or changing a public protocol requirement is MAJOR\n - Removing protocol conformance from a public type is MAJOR\n - Changing a struct to a class (or vice versa) is MAJOR (value vs reference semantics)\n - Making a public initializer failable (init to init?) or vice versa is MAJOR\n - Changing the associated values of an enum case is MAJOR\n - Removing a public typealias is MAJOR\n - Changing access level from public to internal/private is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public types, methods, or properties is MINOR\n - Adding new optional parameters (with default values) is MINOR\n - Adding new enum cases when callers use default in switch is MINOR\n - Adding new protocol extensions with default implementations is MINOR\n - Adding new optional fields to request/response structs is MINOR\n - Adding new error types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields is MINOR\n - Adding new convenience initializers is MINOR\n - Deprecating (but not removing) public APIs with @available(*, deprecated) is MINOR\n\n PATCH (no API surface change):\n - Changes to internal or private members are PATCH\n - Formatting, comments, or documentation changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring URLSession internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in Package.swift is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Changes to test files, xcconfig, or CI configuration are PATCH\n\n {% elif language == \"rust\" %}\n Language-specific breaking change rules for Rust:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public function, struct, enum, or trait is always MAJOR\n - Making a response field optional (T to Option) is MAJOR (callers must handle the Option)\n - New enum variants are MAJOR (Rust match statements must be exhaustive unless using _ wildcard)\n - Adding a new required field to a public struct is MAJOR (unless #[non_exhaustive])\n - Removing a public trait implementation is MAJOR\n - Changing a function's return type is MAJOR\n - Adding a required method to a public trait is MAJOR (all implementations must add it)\n - Changing the type of a public struct field is MAJOR\n - Removing or renaming a public module is MAJOR\n - Making a public type private (pub to pub(crate) or removing pub) is MAJOR\n - Changing a struct from non-exhaustive to exhaustive construction (removing .. Default::default()) is MAJOR\n - Changing generic type parameters or their bounds on public types is MAJOR\n - Changing from Result to Result where E2 is a different error type is MAJOR\n - Removing a public constant or static is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public functions, structs, or enums is MINOR\n - Adding new optional fields to #[non_exhaustive] structs is MINOR\n - Adding new enum variants to #[non_exhaustive] enums is MINOR\n - Adding new trait implementations for existing types is MINOR\n - Adding new public constants or statics is MINOR\n - Adding new methods to existing impl blocks is MINOR\n - Adding new error types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields is MINOR\n - Adding new optional builder methods is MINOR\n - Deprecating (but not removing) public APIs with #[deprecated] is MINOR\n\n PATCH (no API surface change):\n - Changes to pub(crate) or private items are PATCH\n - Cargo.toml metadata, formatting, or comment changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring reqwest/hyper internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in Cargo.toml is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization (serde) config that preserves public types is PATCH\n - Changes to test files, build.rs, or CI configuration are PATCH\n\n {% elif language == \"kotlin\" %}\n Language-specific breaking change rules for Kotlin:\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public function, class, or property is always MAJOR\n - Making a response field nullable (T to T?) is MAJOR (callers must handle null safety operators)\n - New enum values are MAJOR if used in when() expressions without else branch\n - Adding a new required parameter to a public function is MAJOR\n - Changing a method's return type is MAJOR\n - Removing or changing a public constant (const val / companion object val) is MAJOR\n - Changing a class from open to final (or data class to regular class) is MAJOR\n - Removing an interface implementation from a public class is MAJOR\n - Changing generic type parameters or their variance (in/out) on public types is MAJOR\n - Moving a public class to a different package without type aliasing is MAJOR\n - Changing a property from var to val (or removing a setter) is MAJOR\n - Changing a suspend function to non-suspend (or vice versa) is MAJOR\n - Changing sealed class/interface hierarchy (removing subclasses) is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public classes, functions, or extension functions is MINOR\n - Adding new optional parameters (with default values) is MINOR\n - Adding new enum values when callers use else in when() is MINOR\n - Adding new optional properties to data classes is MINOR\n - Adding new exception types is MINOR\n - Adding new environment/server URL constants is MINOR\n - Adding new RequestOptions fields is MINOR\n - Adding new sealed class/interface subtypes is MINOR (if callers have else branch)\n - Deprecating (but not removing) public APIs with @Deprecated is MINOR\n - Adding new companion object functions is MINOR\n\n PATCH (no API surface change):\n - Changes to internal or private members are PATCH\n - KDoc, formatting, or build.gradle changes are PATCH\n - Updating SDK version headers is PATCH\n - Refactoring OkHttp internals without changing observable defaults or behavior is PATCH\n - Updating dependency versions in build.gradle.kts is PATCH\n - Refactoring internal implementation without changing public signatures is PATCH\n - Updating serialization/deserialization (kotlinx.serialization/Moshi) config that preserves public types is PATCH\n - Changes to test files or CI configuration are PATCH\n\n {% else %}\n Language-specific breaking change rules (language: {{ language }}):\n\n MAJOR (breaking):\n - Removing a required response field is always MAJOR\n - Removing or renaming a public class, method, or function is always MAJOR\n - Making a response field optional is MAJOR in statically-typed languages (TypeScript, Java, C#, Swift, Rust, Kotlin, Go), usually MINOR in dynamically-typed ones (Python, Ruby, PHP)\n - New enum values are MAJOR if the language enforces exhaustive matching (TypeScript, Java, C#, Swift, Rust), MINOR otherwise\n - Adding a new required parameter to a public method is MAJOR\n - Changing a method's return type is MAJOR\n - Changing the type of an existing field/property is MAJOR\n - Removing or changing public constants is MAJOR\n\n MINOR (backward-compatible additions):\n - Adding new public APIs (classes, methods, functions) is MINOR\n - Adding new optional parameters is MINOR\n - Adding new optional fields to request/response objects is MINOR\n - Adding new error/exception types is MINOR\n - Deprecating (but not removing) public APIs is MINOR\n\n PATCH (no API surface change):\n - Internal/private changes are PATCH\n - Formatting, documentation, or comment changes are PATCH\n - Dependency version updates are PATCH\n - SDK version header updates are PATCH\n - Refactoring retry/timeout internals without changing observable defaults or behavior is PATCH\n - Refactoring internals without changing public signatures is PATCH\n {% endif %}\n\n Apply these patterns to the diff below. When in doubt between MINOR and PATCH,\n prefer MINOR. When in doubt between MAJOR and MINOR, examine whether existing\n callers would break without any code changes on their side.\n\n Message Format (use this exact structure):\n ```\n : \n\n \n\n Key changes:\n - \n - \n - \n ```\n\n Message Guidelines:\n - Use conventional commit types: feat, fix, refactor, docs, chore, test, style, perf\n - Keep the summary line under 72 characters\n - Write in present tense imperative mood (\"add\" not \"added\" or \"adds\")\n - For breaking changes: include migration instructions in the detailed description\n - For new features: highlight new capabilities in the key changes\n - For PATCH: describe the fix or improvement\n - For NO_CHANGE: use type \"chore\" and state that no functional changes were made\n - Be specific and action-oriented\n - Do not use \"Fern regeneration\" in commit messages - use \"SDK regeneration\" instead\n - NEVER include the literal version \"505.503.4455\" in the commit message - if you see this placeholder\n in the diff, describe changes generically (e.g., \"added X-Fern-SDK-Version header\")\n - The previous version is provided for context only. Do not include it\n literally in the commit message summary line.\n\n {% if prior_changelog %}\n Prior changelog entries (for style reference):\n ---\n {{prior_changelog}}\n ---\n Match the tone and format of these entries in your commit message.\n {% endif %}\n\n {% if spec_commit_message %}\n The API spec change that triggered this SDK generation had the following commit message:\n \"{{spec_commit_message}}\"\n Use this as a hint for understanding the intent of the change, but always verify\n against the actual diff below. The commit message may be vague or inaccurate.\n {% endif %}\n\n Previous version: {{previous_version}}\n SDK language: {{language}}\n\n Git Diff:\n ---\n {{diff}}\n ---\n\n Changelog Entry Guidelines:\n - Write for SDK consumers, not engineers reading the source code\n - MAJOR: explain what broke and how to migrate (\"The `getUser` method has been\n removed. Replace calls with `fetchUser(id)` which returns the same type.\")\n - MINOR: describe the new capability (\"New `createPayment()` method available\n on `PaymentsClient`.\")\n - PATCH: leave empty string — patch changes don't warrant changelog entries\n - Do not use conventional commit prefixes (no \"feat:\", \"fix:\", etc.)\n - Write in third person (\"The SDK now supports...\" not \"Add support for...\")\n\n Remember again that YOU MUST return a structured JSON response with these four fields:\n - message: A git commit message formatted like the example previously provided\n - changelog_entry: A user-facing release note (empty string for PATCH)\n - version_bump: One of: MAJOR, MINOR, PATCH, or NO_CHANGE\n - version_bump_reason: One sentence explaining WHY this bump level was chosen.\n For MAJOR, name the specific breaking symbol(s) and explain why existing callers break.\n For MINOR, name the new capability added.\n For PATCH, describe what was fixed or improved.\n For NO_CHANGE, say \"No functional changes detected.\"\n Example: \"MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`, breaking existing callers.\"\n \"#\n}\n\nclass ConsolidateChangelogResponse {\n consolidated_changelog string\n @description(\"CHANGELOG.md entry in Keep a Changelog format. Group under ### Breaking Changes, ### Added, ### Changed, ### Fixed. Bold symbol names, one tight sentence per bullet. Prose only, no code fences. Append migration action inline for breaking changes.\")\n\n pr_description string\n @description(\"PR description with ## Breaking Changes section (if any) containing ### per breaking change with Before/After code fences and Migration line, then ## What's New section summarizing features in prose paragraphs grouped by theme. Do NOT list every class individually — summarize repetitive changes as a single entry.\")\n\n version_bump_reason string\n @description(\"One sentence explaining WHY the overall version bump was chosen. For MAJOR: name the specific breaking symbol(s). For MINOR: name the new capability. For PATCH: describe the fix. Example: 'MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`.'\")\n}\n\nfunction ConsolidateChangelog(\n raw_entries: string @description(\"Newline-separated raw changelog entries from chunked diff analysis\"),\n version_bump: string @description(\"The overall version bump: MAJOR, MINOR, or PATCH\"),\n language: string @description(\"The SDK programming language, e.g. 'typescript', 'python', 'java'\")\n) -> ConsolidateChangelogResponse {\n client DefaultClient\n\n prompt #\"\n You are a technical writer formatting release notes for a {{language}} SDK.\n\n The raw change notes below are noisy and repetitive — many bullets describe the same\n change across different packages. Deduplicate aggressively: if the same feature appears\n multiple times, merge into one entry.\n\n Raw changelog entries:\n ---\n {{raw_entries}}\n ---\n\n Overall version bump: {{version_bump}}\n\n Produce three outputs:\n\n ---\n\n ## 1. CHANGELOG.md entry (Keep a Changelog format)\n\n - Group under: `### Breaking Changes`, `### Added`, `### Changed`, `### Fixed`\n - Only include sections with entries\n - **Bold the symbol name** first, then one tight sentence for SDK consumers\n - No code fences — prose only\n - For breaking changes, append the migration action inline\n\n ## 2. PR Description\n\n - `## Breaking Changes` section at top (if any)\n - One `###` per breaking change with **Before/After** code fences and a **Migration:** line\n - `## What's New` section summarizing added/changed features in prose paragraphs,\n grouped by theme (e.g. logging, streaming, pagination, builder improvements)\n - Do NOT list every class that got the same method — summarize as a single entry\n\n ## 3. Version Bump Reason\n\n - One sentence explaining WHY the overall version bump ({{version_bump}}) was chosen\n - For MAJOR: name the specific breaking symbol(s) and explain why existing callers break\n - For MINOR: name the new capability added\n - For PATCH: describe what was fixed or improved\n - Example: \"MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`, breaking existing callers.\"\n\n ---\n\n {{ ctx.output_format }}\n \"#\n}\n", "generators.baml": "generator target {\n output_type \"typescript\"\n\n output_dir \"../src/\"\n\n version \"0.219.0\"\n\n default_client_mode async\n}\n", "main.baml": "// BAML client configuration for LLM providers\n\n// OpenAI client configuration\nclient OpenAI {\n provider openai\n options {\n model gpt-4o\n api_key env.OPENAI_API_KEY\n }\n}\n\n// Anthropic client configuration\nclient Anthropic {\n provider anthropic\n options {\n model claude-sonnet-4-5-20250929\n api_key env.ANTHROPIC_API_KEY\n }\n}\n\n// Bedrock client configuration\nclient Bedrock {\n provider aws-bedrock\n options {\n model anthropic.claude-3-5-sonnet-20240620-v1:0\n }\n}\n\n// Fallback client that tries multiple providers\n// TODO(tjb9dc): I wish we didn't have to specify this, we're only going to use one at runtime\nclient DefaultClient {\n provider fallback\n options {\n strategy [\n Anthropic\n OpenAI\n Bedrock\n ]\n }\n}\n", } diff --git a/packages/cli/ai/src/baml_client/parser.ts b/packages/cli/ai/src/baml_client/parser.ts index 230ae2317971..6e06ac1ed436 100644 --- a/packages/cli/ai/src/baml_client/parser.ts +++ b/packages/cli/ai/src/baml_client/parser.ts @@ -23,7 +23,7 @@ import { toBamlError } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type { partial_types } from "./partial_types" import type * as types from "./types" -import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, VersionBump} from "./types" +import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, ConsolidateChangelogResponse, VersionBump} from "./types" import type TypeBuilder from "./type_builder" export class LlmResponseParser { @@ -53,6 +53,29 @@ export class LlmResponseParser { } } + ConsolidateChangelog( + llmResponse: string, + __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } + ): types.ConsolidateChangelogResponse { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.parseLlmResponse( + "ConsolidateChangelog", + llmResponse, + false, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + __env__, + ) as types.ConsolidateChangelogResponse + } catch (error) { + throw toBamlError(error); + } + } + } export class LlmStreamParser { @@ -82,4 +105,27 @@ export class LlmStreamParser { } } + ConsolidateChangelog( + llmResponse: string, + __baml_options__?: { tb?: TypeBuilder, clientRegistry?: ClientRegistry, env?: Record } + ): partial_types.ConsolidateChangelogResponse { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + return this.runtime.parseLlmResponse( + "ConsolidateChangelog", + llmResponse, + true, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __baml_options__?.clientRegistry, + __env__, + ) as partial_types.ConsolidateChangelogResponse + } catch (error) { + throw toBamlError(error); + } + } + } \ No newline at end of file diff --git a/packages/cli/ai/src/baml_client/partial_types.ts b/packages/cli/ai/src/baml_client/partial_types.ts index d0df73c9e701..3d77c1368d3f 100644 --- a/packages/cli/ai/src/baml_client/partial_types.ts +++ b/packages/cli/ai/src/baml_client/partial_types.ts @@ -20,7 +20,7 @@ $ pnpm add @boundaryml/baml import type { Image, Audio, Pdf, Video } from "@boundaryml/baml" import type { Checked, Check } from "./types" -import type { AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, VersionBump } from "./types" +import type { AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, ConsolidateChangelogResponse, VersionBump } from "./types" import type * as types from "./types" /****************************************************************************** @@ -43,5 +43,11 @@ export namespace partial_types { message?: string | null changelog_entry?: string | null version_bump?: types.VersionBump | null + version_bump_reason?: string | null + } + export interface ConsolidateChangelogResponse { + consolidated_changelog?: string | null + pr_description?: string | null + version_bump_reason?: string | null } } \ No newline at end of file diff --git a/packages/cli/ai/src/baml_client/sync_client.ts b/packages/cli/ai/src/baml_client/sync_client.ts index 8bb6271989f9..41a3cd5cf798 100644 --- a/packages/cli/ai/src/baml_client/sync_client.ts +++ b/packages/cli/ai/src/baml_client/sync_client.ts @@ -22,7 +22,7 @@ import type { BamlRuntime, FunctionResult, BamlCtxManager, Image, Audio, Pdf, Vi import { toBamlError, BamlAbortError, ClientRegistry, type HTTPRequest } from "@boundaryml/baml" import type { Checked, Check, RecursivePartialNull as MovedRecursivePartialNull } from "./types" import type * as types from "./types" -import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, VersionBump} from "./types" +import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, ConsolidateChangelogResponse, VersionBump} from "./types" import type TypeBuilder from "./type_builder" import { HttpRequest, HttpStreamRequest } from "./sync_request" import { LlmResponseParser, LlmStreamParser } from "./parser" @@ -147,6 +147,56 @@ export class BamlSyncClient { } } + ConsolidateChangelog( + raw_entries: string,version_bump: string,language: string, + __baml_options__?: BamlCallOptions + ): types.ConsolidateChangelogResponse { + try { + const __options__ = { ...this.bamlOptions, ...(__baml_options__ || {}) } + const __signal__ = __options__.signal; + + if (__signal__?.aborted) { + throw new BamlAbortError('Operation was aborted', __signal__.reason); + } + + // Check if onTick is provided and reject for sync operations + if (__options__.onTick) { + throw new Error("onTick is not supported for synchronous functions. Please use the async client instead."); + } + + const __collector__ = __options__.collector ? (Array.isArray(__options__.collector) ? __options__.collector : [__options__.collector]) : []; + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __options__.clientRegistry; + if (__options__.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__options__.client); + } + + const __raw__ = this.runtime.callFunctionSync( + "ConsolidateChangelog", + { + "raw_entries": raw_entries,"version_bump": version_bump,"language": language + }, + this.ctxManager.cloneContext(), + __options__.tb?.__tb(), + __clientRegistry__, + __collector__, + __options__.tags || {}, + __env__, + __signal__, + __options__.watchers, + ) + return __raw__.parsed(false) as types.ConsolidateChangelogResponse + } catch (error: any) { + throw toBamlError(error); + } + } + } export const b = new BamlSyncClient(DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_RUNTIME, DO_NOT_USE_DIRECTLY_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_CTX) \ No newline at end of file diff --git a/packages/cli/ai/src/baml_client/sync_request.ts b/packages/cli/ai/src/baml_client/sync_request.ts index 56c69c0e4840..6cdb7263df9c 100644 --- a/packages/cli/ai/src/baml_client/sync_request.ts +++ b/packages/cli/ai/src/baml_client/sync_request.ts @@ -22,7 +22,7 @@ import type { BamlRuntime, BamlCtxManager, Image, Audio, Pdf, Video } from "@bou import { toBamlError, HTTPRequest, ClientRegistry } from "@boundaryml/baml" import type { Checked, Check } from "./types" import type * as types from "./types" -import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, VersionBump} from "./types" +import type {AnalyzeCommitDiffRequest, AnalyzeCommitDiffResponse, ConsolidateChangelogResponse, VersionBump} from "./types" import type TypeBuilder from "./type_builder" import type * as events from "./events" @@ -71,6 +71,39 @@ export class HttpRequest { } } + ConsolidateChangelog( + raw_entries: string,version_bump: string,language: string, + __baml_options__?: BamlCallOptions + ): HTTPRequest { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return this.runtime.buildRequestSync( + "ConsolidateChangelog", + { + "raw_entries": raw_entries,"version_bump": version_bump,"language": language + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + false, + __env__, + ) + } catch (error) { + throw toBamlError(error); + } + } + } export class HttpStreamRequest { @@ -110,4 +143,37 @@ export class HttpStreamRequest { } } + ConsolidateChangelog( + raw_entries: string,version_bump: string,language: string, + __baml_options__?: BamlCallOptions + ): HTTPRequest { + try { + const __rawEnv__ = __baml_options__?.env ? { ...process.env, ...__baml_options__.env } : { ...process.env }; + const __env__: Record = Object.fromEntries( + Object.entries(__rawEnv__).filter(([_, value]) => value !== undefined) as [string, string][] + ); + + // Resolve client option to clientRegistry (client takes precedence) + let __clientRegistry__ = __baml_options__?.clientRegistry; + if (__baml_options__?.client) { + __clientRegistry__ = __clientRegistry__ || new ClientRegistry(); + __clientRegistry__.setPrimary(__baml_options__.client); + } + + return this.runtime.buildRequestSync( + "ConsolidateChangelog", + { + "raw_entries": raw_entries,"version_bump": version_bump,"language": language + }, + this.ctxManager.cloneContext(), + __baml_options__?.tb?.__tb(), + __clientRegistry__, + true, + __env__, + ) + } catch (error) { + throw toBamlError(error); + } + } + } \ No newline at end of file diff --git a/packages/cli/ai/src/baml_client/type_builder.ts b/packages/cli/ai/src/baml_client/type_builder.ts index 382465a3a6ca..32953d86c811 100644 --- a/packages/cli/ai/src/baml_client/type_builder.ts +++ b/packages/cli/ai/src/baml_client/type_builder.ts @@ -29,7 +29,9 @@ export default class TypeBuilder { AnalyzeCommitDiffRequest: ClassViewer<'AnalyzeCommitDiffRequest', "diff">; - AnalyzeCommitDiffResponse: ClassViewer<'AnalyzeCommitDiffResponse', "message" | "changelog_entry" | "version_bump">; + AnalyzeCommitDiffResponse: ClassViewer<'AnalyzeCommitDiffResponse', "message" | "changelog_entry" | "version_bump" | "version_bump_reason">; + + ConsolidateChangelogResponse: ClassViewer<'ConsolidateChangelogResponse', "consolidated_changelog" | "pr_description" | "version_bump_reason">; VersionBump: EnumViewer<'VersionBump', "MAJOR" | "MINOR" | "PATCH" | "NO_CHANGE">; @@ -38,7 +40,7 @@ export default class TypeBuilder { constructor() { this.tb = new _TypeBuilder({ classes: new Set([ - "AnalyzeCommitDiffRequest","AnalyzeCommitDiffResponse", + "AnalyzeCommitDiffRequest","AnalyzeCommitDiffResponse","ConsolidateChangelogResponse", ]), enums: new Set([ "VersionBump", @@ -51,7 +53,11 @@ export default class TypeBuilder { ]); this.AnalyzeCommitDiffResponse = this.tb.classViewer("AnalyzeCommitDiffResponse", [ - "message","changelog_entry","version_bump", + "message","changelog_entry","version_bump","version_bump_reason", + ]); + + this.ConsolidateChangelogResponse = this.tb.classViewer("ConsolidateChangelogResponse", [ + "consolidated_changelog","pr_description","version_bump_reason", ]); diff --git a/packages/cli/ai/src/baml_client/types.ts b/packages/cli/ai/src/baml_client/types.ts index a6de96210300..b998aa85ccaa 100644 --- a/packages/cli/ai/src/baml_client/types.ts +++ b/packages/cli/ai/src/baml_client/types.ts @@ -63,5 +63,13 @@ export interface AnalyzeCommitDiffResponse { message: string changelog_entry: string version_bump: VersionBump + version_bump_reason: string + +} + +export interface ConsolidateChangelogResponse { + consolidated_changelog: string + pr_description: string + version_bump_reason: string } diff --git a/packages/cli/cli/src/commands/sdk-diff/sdkDiffCommand.ts b/packages/cli/cli/src/commands/sdk-diff/sdkDiffCommand.ts index 50b71b4a469d..c2f705e95ff6 100644 --- a/packages/cli/cli/src/commands/sdk-diff/sdkDiffCommand.ts +++ b/packages/cli/cli/src/commands/sdk-diff/sdkDiffCommand.ts @@ -100,7 +100,8 @@ export async function sdkDiffCommand({ return { message: "No changes detected between the directories", changelog_entry: "", - version_bump: VersionBump.NO_CHANGE + version_bump: VersionBump.NO_CHANGE, + version_bump_reason: "No functional changes detected." }; } @@ -146,6 +147,7 @@ export async function sdkDiffCommand({ // Multi-chunk analysis — analyze each chunk, merge results let bestBump: string = VersionBump.NO_CHANGE; let bestMessage = ""; + let bestVersionBumpReason = ""; const allChangelogEntries: string[] = []; for (let i = 0; i < cappedChunks.length; i++) { @@ -169,6 +171,7 @@ export async function sdkDiffCommand({ if (bestBump !== prevBest) { bestMessage = chunkAnalysis.message; + bestVersionBumpReason = chunkAnalysis.version_bump_reason?.trim() || ""; } const entry = chunkAnalysis.changelog_entry?.trim(); @@ -188,17 +191,37 @@ export async function sdkDiffCommand({ return { version_bump: VersionBump.NO_CHANGE, message: "No changes detected between the directories", - changelog_entry: "" + changelog_entry: "", + version_bump_reason: "No functional changes detected." }; } + let changelogEntry: string; + let versionBumpReason = bestVersionBumpReason; + if (allChangelogEntries.length > 1) { + // Consolidate repetitive multi-chunk entries via AI rollup + const rawEntries = allChangelogEntries.map((e) => (e.startsWith("- ") ? e : `- ${e}`)).join("\n"); + try { + context.logger.debug(`Consolidating ${allChangelogEntries.length} changelog entries via AI rollup`); + const rollup = await bamlClient.ConsolidateChangelog(rawEntries, bestBump, "unknown"); + changelogEntry = rollup.consolidated_changelog?.trim() || rawEntries; + versionBumpReason = rollup.version_bump_reason?.trim() || ""; + } catch (rollupError) { + context.logger.warn( + `Changelog consolidation failed, using raw entries: ${rollupError instanceof Error ? rollupError.message : String(rollupError)}` + ); + changelogEntry = rawEntries; + } + } else { + changelogEntry = allChangelogEntries[0] ?? ""; + versionBumpReason = bestVersionBumpReason; + } + return { version_bump: bestBump as VersionBump, message: bestMessage || "SDK regeneration", - changelog_entry: - allChangelogEntries.length > 1 - ? allChangelogEntries.map((e) => (e.startsWith("- ") ? e : `- ${e}`)).join("\n") - : (allChangelogEntries[0] ?? "") + changelog_entry: changelogEntry, + version_bump_reason: versionBumpReason }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index 5508a070947a..f10ed787f313 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,19 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 4.29.0 + changelogEntry: + - summary: | + Add AI changelog rollup for multi-chunk diff analysis. When large diffs + are split into multiple chunks, the resulting changelog entries are now + consolidated via a dedicated AI call that deduplicates repetitive bullets, + groups changes under Keep a Changelog headers (Breaking Changes, Added, + Changed, Fixed), and produces a structured PR description with + Before/After code fences for breaking changes. Also adds a + `version_bump_reason` field to AI analysis responses, providing an + explicit one-sentence justification for why MAJOR/MINOR/PATCH was chosen. + type: feat + createdAt: "2026-03-13" + irVersion: 65 + - version: 4.28.0 changelogEntry: - summary: | @@ -22,7 +37,6 @@ type: feat createdAt: "2026-03-13" irVersion: 65 - - version: 4.27.0 changelogEntry: - summary: | @@ -33,7 +47,6 @@ type: feat createdAt: "2026-03-13" irVersion: 65 - - version: 4.26.1 changelogEntry: - summary: | diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningCache.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningCache.ts index 503b8b96b382..8629493e5bd6 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningCache.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningCache.ts @@ -15,6 +15,10 @@ export interface CachedAnalysis { message: string; /** User-facing changelog entry from the AI. Empty string for PATCH, present for MINOR/MAJOR. */ changelogEntry: string; + /** PR description with breaking changes (Before/After code fences) and What's New sections. */ + prDescription?: string; + /** One sentence explaining WHY the version bump was chosen. */ + versionBumpReason?: string; } /** diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts index 08ce6894945e..444481313426 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts @@ -29,6 +29,16 @@ export interface AutoVersionResult { * Undefined for PATCH changes, present for MINOR/MAJOR. */ changelogEntry?: string; + /** + * PR description with breaking changes (Before/After code fences) and What's New sections. + * Undefined for PATCH changes or when consolidation is not performed. + */ + prDescription?: string; + /** + * One sentence explaining WHY the version bump was chosen. + * E.g., "MAJOR because `parserCreateJob` InputStream overloads were removed from `RawLabReportClient`." + */ + versionBumpReason?: string; } /** diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/GenerationRunner.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/GenerationRunner.ts index 023dbca403f6..7275871b2dd5 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/GenerationRunner.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/GenerationRunner.ts @@ -136,6 +136,8 @@ export class GenerationRunner { shouldCommit: boolean; autoVersioningCommitMessage?: string; autoVersioningChangelogEntry?: string; + autoVersioningPrDescription?: string; + autoVersioningVersionBumpReason?: string; }> { context.logger.info(`Starting generation for ${generatorInvocation.name}`); diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts index f92cf59e56be..7896b9312177 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts @@ -87,6 +87,8 @@ export class LocalTaskHandler { shouldCommit: boolean; autoVersioningCommitMessage?: string; autoVersioningChangelogEntry?: string; + autoVersioningPrDescription?: string; + autoVersioningVersionBumpReason?: string; }> { const isFernIgnorePresent = await this.isFernIgnorePresent(); const isExistingGitRepo = await this.isGitRepository(); @@ -136,7 +138,9 @@ export class LocalTaskHandler { return { shouldCommit: false, autoVersioningCommitMessage: undefined, - autoVersioningChangelogEntry: undefined + autoVersioningChangelogEntry: undefined, + autoVersioningPrDescription: undefined, + autoVersioningVersionBumpReason: undefined }; } // Replace placeholder version with computed version @@ -148,7 +152,9 @@ export class LocalTaskHandler { return { shouldCommit: true, autoVersioningCommitMessage: autoVersionResult.commitMessage, - autoVersioningChangelogEntry: autoVersionResult.changelogEntry + autoVersioningChangelogEntry: autoVersionResult.changelogEntry, + autoVersioningPrDescription: autoVersionResult.prDescription, + autoVersioningVersionBumpReason: autoVersionResult.versionBumpReason }; } return { shouldCommit: true, autoVersioningCommitMessage: undefined }; @@ -277,6 +283,7 @@ export class LocalTaskHandler { // We process ALL chunks so that every changelog entry is captured. let bestBump: string = VersionBump.NO_CHANGE; let bestMessage = ""; + let bestVersionBumpReason: string | undefined; const allChangelogEntries: string[] = []; for (let i = 0; i < cappedChunks.length; i++) { @@ -305,9 +312,10 @@ export class LocalTaskHandler { const prevBest = bestBump; bestBump = maxVersionBump(bestBump, chunkAnalysis.versionBump); - // Keep the commit message from the chunk that produced the highest bump + // Keep the commit message and bump reason from the chunk that produced the highest bump if (bestBump !== prevBest) { bestMessage = chunkAnalysis.message; + bestVersionBumpReason = chunkAnalysis.versionBumpReason; } // Collect all non-empty changelog entries so the final @@ -326,13 +334,41 @@ export class LocalTaskHandler { if (bestBump === VersionBump.NO_CHANGE) { analysis = null; } else { + let changelogEntry: string; + let prDescription: string | undefined; + let versionBumpReason: string | undefined = bestVersionBumpReason; + if (allChangelogEntries.length > 1) { + // Consolidate repetitive multi-chunk entries via AI rollup + const rawEntries = allChangelogEntries + .map((e) => (e.startsWith("- ") ? e : `- ${e}`)) + .join("\n"); + try { + this.context.logger.debug( + `Consolidating ${allChangelogEntries.length} changelog entries via AI rollup` + ); + const rollup = await BamlClient.withOptions({ + clientRegistry: await this.getClientRegistry() + }).ConsolidateChangelog(rawEntries, bestBump, this.generatorLanguage ?? "unknown"); + changelogEntry = rollup.consolidated_changelog?.trim() || rawEntries; + prDescription = rollup.pr_description?.trim() || undefined; + versionBumpReason = rollup.version_bump_reason?.trim() || undefined; + } catch (rollupError) { + this.context.logger.warn( + `Changelog consolidation failed, using raw entries: ${rollupError instanceof Error ? rollupError.message : String(rollupError)}` + ); + changelogEntry = rawEntries; + } + } else { + changelogEntry = allChangelogEntries[0] ?? ""; + versionBumpReason = bestVersionBumpReason; + } + analysis = { versionBump: bestBump as VersionBump, message: bestMessage, - changelogEntry: - allChangelogEntries.length > 1 - ? allChangelogEntries.map((e) => (e.startsWith("- ") ? e : `- ${e}`)).join("\n") - : (allChangelogEntries[0] ?? "") + changelogEntry, + prDescription, + versionBumpReason }; } } @@ -366,6 +402,8 @@ export class LocalTaskHandler { const finalBump = analysis.versionBump; const finalMessage = analysis.message; const finalChangelogEntry = analysis.changelogEntry; + const finalPrDescription = analysis.prDescription; + const finalVersionBumpReason = analysis.versionBumpReason; const newVersion = this.incrementVersion(previousVersion, finalBump); this.context.logger.info(`Version bump: ${finalBump}, new version: ${newVersion}`); @@ -374,11 +412,15 @@ export class LocalTaskHandler { // changelogEntry is populated for MINOR/MAJOR, undefined for PATCH (empty string from AI) const changelogEntry = finalChangelogEntry?.trim() || undefined; + const prDescription = finalPrDescription?.trim() || undefined; + const versionBumpReason = finalVersionBumpReason?.trim() || undefined; return { version: newVersion, commitMessage, - changelogEntry + changelogEntry, + prDescription, + versionBumpReason }; } catch (error) { if (error instanceof AutoVersioningException) { @@ -444,7 +486,8 @@ export class LocalTaskHandler { return { versionBump: analysis.version_bump, message: analysis.message, - changelogEntry: analysis.changelog_entry + changelogEntry: analysis.changelog_entry, + versionBumpReason: analysis.version_bump_reason }; }; diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts index d62a5537ef2d..f42def09eed3 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/runGenerator.ts @@ -112,6 +112,8 @@ export async function writeFilesToDiskAndRunGenerator({ shouldCommit: boolean; autoVersioningCommitMessage?: string; autoVersioningChangelogEntry?: string; + autoVersioningPrDescription?: string; + autoVersioningVersionBumpReason?: string; }> { const { latest, migrated } = await getIntermediateRepresentation({ workspace, diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/runLocalGenerationForWorkspace.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/runLocalGenerationForWorkspace.ts index d689b6853688..609ee9ad1e59 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/runLocalGenerationForWorkspace.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/runLocalGenerationForWorkspace.ts @@ -297,39 +297,40 @@ export async function runLocalGenerationForWorkspace({ // NOTE(tjb9dc): Important that we get a new temp dir per-generator, as we don't want their local files to collide. const workspaceTempDir = await getWorkspaceTempDir(); - const { shouldCommit, autoVersioningCommitMessage, autoVersioningChangelogEntry } = - await writeFilesToDiskAndRunGenerator({ - organization: projectConfig.organization, - absolutePathToFernConfig: projectConfig._absolutePath, - workspace: fernWorkspace, - generatorInvocation, - absolutePathToLocalOutput, - absolutePathToLocalSnippetJSON, - absolutePathToLocalSnippetTemplateJSON: undefined, - version, - audiences: generatorGroup.audiences, - workspaceTempDir, - keepDocker, - context: interactiveTaskContext, - irVersionOverride: generatorInvocation.irVersionOverride, - outputVersionOverride: version, - writeUnitTests: true, - generateOauthClients: organization.ok - ? (organization?.body.oauthClientEnabled ?? false) - : false, - generatePaginatedClients: organization.ok - ? (organization?.body.paginationEnabled ?? false) - : false, - includeOptionalRequestPropertyExamples: false, - inspect, - executionEnvironment: undefined, // This should use the Docker fallback with proper image name - ir: intermediateRepresentation, - whiteLabel: organization.ok ? organization.body.isWhitelabled : false, - runner, - ai, - autoVersioningCache, - absolutePathToSpecRepo: dirname(workspace.absoluteFilePath) - }); + const { + shouldCommit, + autoVersioningCommitMessage, + autoVersioningChangelogEntry, + autoVersioningPrDescription, + autoVersioningVersionBumpReason + } = await writeFilesToDiskAndRunGenerator({ + organization: projectConfig.organization, + absolutePathToFernConfig: projectConfig._absolutePath, + workspace: fernWorkspace, + generatorInvocation, + absolutePathToLocalOutput, + absolutePathToLocalSnippetJSON, + absolutePathToLocalSnippetTemplateJSON: undefined, + version, + audiences: generatorGroup.audiences, + workspaceTempDir, + keepDocker, + context: interactiveTaskContext, + irVersionOverride: generatorInvocation.irVersionOverride, + outputVersionOverride: version, + writeUnitTests: true, + generateOauthClients: organization.ok ? (organization?.body.oauthClientEnabled ?? false) : false, + generatePaginatedClients: organization.ok ? (organization?.body.paginationEnabled ?? false) : false, + includeOptionalRequestPropertyExamples: false, + inspect, + executionEnvironment: undefined, // This should use the Docker fallback with proper image name + ir: intermediateRepresentation, + whiteLabel: organization.ok ? organization.body.isWhitelabled : false, + runner, + ai, + autoVersioningCache, + absolutePathToSpecRepo: dirname(workspace.absoluteFilePath) + }); interactiveTaskContext.logger.info(chalk.green("Wrote files to " + absolutePathToLocalOutput)); @@ -354,6 +355,8 @@ export async function runLocalGenerationForWorkspace({ branch: selfhostedGithubConfig.branch, commitMessage: autoVersioningCommitMessage, changelogEntry: autoVersioningChangelogEntry, + prDescription: autoVersioningPrDescription, + versionBumpReason: autoVersioningVersionBumpReason, previewMode: selfhostedGithubConfig.previewMode, generatorName: generatorInvocation.name }, diff --git a/packages/generator-cli/src/pipeline/github/parseCommitMessage.ts b/packages/generator-cli/src/pipeline/github/parseCommitMessage.ts index a8a95a7a7550..2c7c715f6338 100644 --- a/packages/generator-cli/src/pipeline/github/parseCommitMessage.ts +++ b/packages/generator-cli/src/pipeline/github/parseCommitMessage.ts @@ -1,10 +1,20 @@ export function parseCommitMessageForPR( commitMessage: string, - changelogEntry?: string + changelogEntry?: string, + prDescription?: string, + versionBumpReason?: string ): { prTitle: string; prBody: string } { const lines = commitMessage.split("\n"); const prTitle = lines[0]?.trim() || "SDK Generation"; const bodyFromCommit = lines.slice(1).join("\n").trim() || "Automated SDK generation by Fern"; - const prBody = changelogEntry?.trim() || bodyFromCommit; + + // Prefer prDescription (structured with Before/After code fences) over changelogEntry + let prBody = prDescription?.trim() || changelogEntry?.trim() || bodyFromCommit; + + // Prepend version bump reason if available + if (versionBumpReason?.trim()) { + prBody = `**Version Bump:** ${versionBumpReason.trim()}\n\n${prBody}`; + } + return { prTitle, prBody }; } diff --git a/packages/generator-cli/src/pipeline/steps/GithubStep.ts b/packages/generator-cli/src/pipeline/steps/GithubStep.ts index db94dc75245c..6486305656ff 100644 --- a/packages/generator-cli/src/pipeline/steps/GithubStep.ts +++ b/packages/generator-cli/src/pipeline/steps/GithubStep.ts @@ -176,7 +176,12 @@ export class GithubStep extends BaseStep { } const finalCommitMessage = this.config.commitMessage ?? "SDK Generation"; - const { prTitle, prBody } = parseCommitMessageForPR(finalCommitMessage, this.config.changelogEntry); + const { prTitle, prBody } = parseCommitMessageForPR( + finalCommitMessage, + this.config.changelogEntry, + this.config.prDescription, + this.config.versionBumpReason + ); const replaySection = formatReplayPrBody(replayResult, { branchName: prBranch, repoUri: this.config.uri }); const enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody; diff --git a/packages/generator-cli/src/pipeline/types.ts b/packages/generator-cli/src/pipeline/types.ts index 8b79dd3db62a..cb638b2606c0 100644 --- a/packages/generator-cli/src/pipeline/types.ts +++ b/packages/generator-cli/src/pipeline/types.ts @@ -44,6 +44,10 @@ export interface GithubStepConfig { commitMessage?: string; /** User-facing changelog entry for PR body. When present, used instead of commit message body. */ changelogEntry?: string; + /** Structured PR description with Before/After code fences for breaking changes. Takes priority over changelogEntry for PR body. */ + prDescription?: string; + /** One-sentence justification for WHY the version bump was chosen. Prepended to PR body when present. */ + versionBumpReason?: string; /** Skip push/PR creation, just prepare branches locally */ previewMode?: boolean; /** Generator name for namespaced fern-generation-base tag */ From 253fc6522008f11a4bf7c3e9049585bae160d46a Mon Sep 17 00:00:00 2001 From: Fern Support <126544928+fern-support@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:13:58 -0400 Subject: [PATCH 29/29] chore(typescript): update ts-sdk seed (#13544) Co-authored-by: Swimburger --- .../package.json | 5 +- .../no-custom-config/src/Client.ts | 8 ++-- .../core/fetcher/makePassthroughRequest.ts | 48 ++++++++++++++----- .../fetcher/makePassthroughRequest.test.ts | 48 +++++++++++++++++++ 4 files changed, 92 insertions(+), 17 deletions(-) diff --git a/seed/ts-sdk/oauth-client-credentials-custom/package.json b/seed/ts-sdk/oauth-client-credentials-custom/package.json index de75519a69bf..ad0bf621cc88 100644 --- a/seed/ts-sdk/oauth-client-credentials-custom/package.json +++ b/seed/ts-sdk/oauth-client-credentials-custom/package.json @@ -2,7 +2,10 @@ "name": "@fern/oauth-client-credentials-custom", "version": "0.0.1", "private": false, - "repository": "git+https://github.com/oauth-client-credentials-custom/fern", + "repository": { + "type": "git", + "url": "git+https://github.com/oauth-client-credentials-custom/fern.git" + }, "type": "commonjs", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.mjs", diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts index c6fcdf16e06c..05185f955a5f 100644 --- a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/Client.ts @@ -44,20 +44,20 @@ export class SeedOauthClientCredentialsClient { /** * Make a passthrough request using the SDK's configured auth, retry, logging, etc. * This is useful for making requests to endpoints not yet supported in the SDK. - * The URL can be a full URL or a relative path (resolved against the configured base URL). + * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL. * - * @param {string} url - The URL or path to request. + * @param {Request | string | URL} input - The URL, path, or Request object. * @param {RequestInit} init - Standard fetch RequestInit options. * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal). * @returns {Promise} A standard Response object. */ public async fetch( - url: string, + input: Request | string | URL, init?: RequestInit, requestOptions?: core.PassthroughRequest.RequestOptions, ): Promise { return core.makePassthroughRequest( - url, + input, init, { environment: this._options.environment, diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts index 35d53a3a6ac8..f5ba761400f8 100644 --- a/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/src/core/fetcher/makePassthroughRequest.ts @@ -48,20 +48,44 @@ export declare namespace PassthroughRequest { * Makes a passthrough HTTP request using the SDK's configuration (auth, retry, logging, etc.) * while mimicking the standard `fetch` API. * - * @param url - The URL or path to request. If a relative path, it will be resolved against the configured base URL. + * @param input - The URL, path, or Request object. If a relative path, it will be resolved against the configured base URL. * @param init - Standard RequestInit options (method, headers, body, signal, etc.) * @param clientOptions - SDK client options (auth, default headers, logging, etc.) * @param requestOptions - Per-request overrides (timeout, retries, extra headers, abort signal). * @returns A standard Response object. */ export async function makePassthroughRequest( - url: string, + input: Request | string | URL, init: RequestInit | undefined, clientOptions: PassthroughRequest.ClientOptions, requestOptions?: PassthroughRequest.RequestOptions, ): Promise { const logger = createLogger(clientOptions.logging); + // Extract URL and default init properties from Request object if provided + let url: string; + let effectiveInit: RequestInit | undefined = init; + if (input instanceof Request) { + url = input.url; + // If no explicit init provided, extract properties from the Request object + if (init == null) { + effectiveInit = { + method: input.method, + headers: Object.fromEntries(input.headers.entries()), + body: input.body, + signal: input.signal, + credentials: input.credentials, + cache: input.cache as RequestCache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + mode: input.mode, + }; + } + } else { + url = input instanceof URL ? input.toString() : input; + } + // Resolve the base URL const baseUrl = (clientOptions.baseUrl != null ? await Supplier.get(clientOptions.baseUrl) : undefined) ?? @@ -99,13 +123,13 @@ export async function makePassthroughRequest( } // Apply user-provided headers from init - if (init?.headers != null) { + if (effectiveInit?.headers != null) { const initHeaders = - init.headers instanceof Headers - ? Object.fromEntries(init.headers.entries()) - : Array.isArray(init.headers) - ? Object.fromEntries(init.headers) - : init.headers; + effectiveInit.headers instanceof Headers + ? Object.fromEntries(effectiveInit.headers.entries()) + : Array.isArray(effectiveInit.headers) + ? Object.fromEntries(effectiveInit.headers) + : effectiveInit.headers; for (const [key, value] of Object.entries(initHeaders)) { if (value != null) { mergedHeaders[key.toLowerCase()] = value; @@ -120,12 +144,12 @@ export async function makePassthroughRequest( } } - const method = init?.method ?? "GET"; - const body = init?.body; + const method = effectiveInit?.method ?? "GET"; + const body = effectiveInit?.body; const timeoutInSeconds = requestOptions?.timeoutInSeconds ?? clientOptions.timeoutInSeconds; const timeoutMs = timeoutInSeconds != null ? timeoutInSeconds * 1000 : undefined; const maxRetries = requestOptions?.maxRetries ?? clientOptions.maxRetries; - const abortSignal = requestOptions?.abortSignal ?? init?.signal ?? undefined; + const abortSignal = requestOptions?.abortSignal ?? effectiveInit?.signal ?? undefined; const fetchFn = clientOptions.fetch ?? (await getFetchFn()); if (logger.isDebug()) { @@ -146,7 +170,7 @@ export async function makePassthroughRequest( body ?? undefined, timeoutMs, abortSignal, - init?.credentials === "include", + effectiveInit?.credentials === "include", undefined, // duplex false, // disableCache ), diff --git a/seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts b/seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts index f36490719ac3..1850d1fda959 100644 --- a/seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts +++ b/seed/ts-sdk/oauth-client-credentials/no-custom-config/tests/unit/fetcher/makePassthroughRequest.test.ts @@ -71,6 +71,14 @@ describe("makePassthroughRequest", () => { const [calledUrl] = mockFetch.mock.calls[0]; expect(calledUrl).toBe("https://other.example.com/path"); }); + + it("should accept a URL object", async () => { + await makePassthroughRequest(new URL("https://api.example.com/v1/users"), undefined, { + fetch: mockFetch, + }); + const [calledUrl] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/users"); + }); }); describe("header merge order", () => { @@ -333,6 +341,46 @@ describe("makePassthroughRequest", () => { }); }); + describe("Request object input", () => { + it("should extract URL from Request object", async () => { + const request = new Request("https://api.example.com/v1/resource", { method: "POST" }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [calledUrl, calledOptions] = mockFetch.mock.calls[0]; + expect(calledUrl).toBe("https://api.example.com/v1/resource"); + expect(calledOptions.method).toBe("POST"); + }); + + it("should extract headers from Request object when no init provided", async () => { + const request = new Request("https://api.example.com", { + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest(request, undefined, { + fetch: mockFetch, + }); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.headers["x-from-request"]).toBe("request-value"); + }); + + it("should use explicit init over Request object properties", async () => { + const request = new Request("https://api.example.com", { + method: "POST", + headers: { "X-From-Request": "request-value" }, + }); + await makePassthroughRequest( + request, + { method: "PUT", headers: { "X-From-Init": "init-value" } }, + { fetch: mockFetch }, + ); + const [, calledOptions] = mockFetch.mock.calls[0]; + expect(calledOptions.method).toBe("PUT"); + expect(calledOptions.headers["x-from-init"]).toBe("init-value"); + // Request headers should NOT be present since explicit init was provided + expect(calledOptions.headers["x-from-request"]).toBeUndefined(); + }); + }); + describe("SDK default header suppliers", () => { it("should resolve supplier functions for SDK default headers", async () => { await makePassthroughRequest("https://api.example.com", undefined, {